diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index fc817cba9..38d6cb2b6 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -34,6 +34,10 @@ on: branches: - develop +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + env: HOMEBREW_NO_ANALYTICS: "ON" # Make Homebrew installation a little quicker HOMEBREW_NO_AUTO_UPDATE: "ON" @@ -55,7 +59,6 @@ jobs: rai: [1.2.7] # Redis AI versions py_v: [3.8, 3.9, '3.10'] # Python versions - env: SMARTSIM_REDISAI: ${{ matrix.rai }} @@ -107,13 +110,8 @@ jobs: - 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 - - name: Install ML Runtimes with Smart (with pt and tf support) - if: (matrix.py_v == '3.10') - run: smart build --device cpu -v - - name: Run mypy run: | python -m pip install .[mypy] diff --git a/README.md b/README.md index df671ef02..4754b547f 100644 --- a/README.md +++ b/README.md @@ -100,8 +100,8 @@ before using it on your system. Each tutorial is a Jupyter notebook that can be which will run a jupyter lab with the tutorials, SmartSim, and SmartRedis installed. ```bash -docker pull ghcr.io/craylabs/smartsim-tutorials:v0.4.1 -docker run -p 8888:8888 ghcr.io/craylabs/smartsim-tutorials:v0.4.1 +docker pull ghcr.io/craylabs/smartsim-tutorials:latest +docker run -p 8888:8888 ghcr.io/craylabs/smartsim-tutorials:latest # click on link to open jupyter lab ``` @@ -452,8 +452,8 @@ Each tutorial is a Jupyter notebook that can be run through the which will run a jupyter lab with the tutorials, SmartSim, and SmartRedis installed. ```bash -docker pull ghcr.io/craylabs/smartsim-tutorials:v1 -docker run -p 8888:8888 ghcr.io/craylabs/smartsim-tutorials:v0.4.1 +docker pull ghcr.io/craylabs/smartsim-tutorials:latest +docker run -p 8888:8888 ghcr.io/craylabs/smartsim-tutorials:latest ``` Each of the following examples can be found in the [SmartSim documentation](https://www.craylabs.org/docs/tutorials/getting_started/getting_started.html). @@ -640,15 +640,15 @@ from C, C++, Fortran and Python with the SmartRedis Clients: 1.2.7 PyTorch - 1.11.x + 2.0.1 TensorFlow\Keras - 2.8.x + 2.13.1 ONNX - 1.11.x + 1.14.1 diff --git a/doc/_static/custom_tab_style.css b/doc/_static/custom_tab_style.css new file mode 100644 index 000000000..f31e13667 --- /dev/null +++ b/doc/_static/custom_tab_style.css @@ -0,0 +1,7 @@ +.sphinx-tabs-panel { + background-color: inherit; +} + +.sphinx-tabs-tab[aria-selected="true"] { + background-color: inherit; +} \ No newline at end of file diff --git a/doc/api/smartsim_api.rst b/doc/api/smartsim_api.rst index 5136c8aa5..3d6da1ee7 100644 --- a/doc/api/smartsim_api.rst +++ b/doc/api/smartsim_api.rst @@ -34,6 +34,8 @@ Experiment :members: +.. _settings-info: + Settings ======== @@ -421,6 +423,7 @@ Orchestrator :inherited-members: :undoc-members: +.. _model_api: Model ===== diff --git a/doc/changelog.rst b/doc/changelog.rst index befb9ee37..bdcf6cb5f 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -17,6 +17,26 @@ Development branch To be released at some future point in time +Description + +- Override the sphinx-tabs extension background color +- Updated SmartSim's machine learning backends +- Added ONNX support for Python 3.10 + +Detailed Notes + +- The sphinx-tabs documentation extension uses a white background for the tabs component. + A custom CSS for those components to inherit the overall theme color has + been added. (SmartSim-PR453_) +- Updated SmartSim's machine learning backends to PyTorch 2.0.1, Tensorflow + 2.13.1, ONNX 1.14.1, and ONNX Runtime 1.16.1. As a result of this change, + there is now an available ONNX wheel for use with Python 3.10. + (SmartSim-PR451_) + + +.. _SmartSim-PR451: https://github.com/CrayLabs/SmartSim/pull/451 +.. _SmartSim-PR453: https://github.com/CrayLabs/SmartSim/pull/453 + 0.6.0 ----- diff --git a/doc/conf.py b/doc/conf.py index 908b9534f..7817d2f2e 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -100,6 +100,11 @@ "extra_footer": extra_footer, } +# Use a custom style sheet to avoid the sphinx-tabs extension from using +# white background with dark themes. If sphinx-tabs updates its +# static/tabs.css, this may need to be updated. +html_css_files = ['custom_tab_style.css'] + autoclass_content = 'both' add_module_names = False diff --git a/doc/index.rst b/doc/index.rst index 91a7ee1ba..803791921 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -30,6 +30,7 @@ :caption: SmartSim experiment + model orchestrator launchers ml_features diff --git a/doc/installation_instructions/basic.rst b/doc/installation_instructions/basic.rst index 3874eb961..e9f9542cb 100644 --- a/doc/installation_instructions/basic.rst +++ b/doc/installation_instructions/basic.rst @@ -84,15 +84,14 @@ 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.7 (default) | 2.0.1 | 2.13.1 | 1.16.3 | +------------------+----------+-------------+---------------+ TensorFlow_ 2.0 and Keras_ are supported through `graph freezing`_. ScikitLearn_ and Spark_ models are supported by SmartSim as well through the use of the ONNX_ runtime (which is not built by -default due to issues with glibc on a variety of Linux -platforms and lack of support for Mac OS X). +default due to issues with glibc on a variety of Linux platforms). .. _Spark: https://spark.apache.org/mllib/ .. _Keras: https://keras.io @@ -242,9 +241,9 @@ SmartSim does. * - Platform - Python Versions * - MacOS - - 3.7 - 3.10 + - 3.8 - 3.10 * - Linux - - 3.7 - 3.10 + - 3.8 - 3.10 The Python client for SmartRedis is installed through ``pip`` as follows: diff --git a/doc/ml_features.rst b/doc/ml_features.rst index 51027e7ae..6096f005e 100644 --- a/doc/ml_features.rst +++ b/doc/ml_features.rst @@ -169,7 +169,7 @@ to the DB using the SmartRedis client. .. group-tab:: PyTorch - PyTorch requires models to be `jit-traced `__. + 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. @@ -239,7 +239,7 @@ it can be uploaded to the DB using the SmartRedis client. .. group-tab:: PyTorch - PyTorch requires models to be `jit-traced `__. + 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``. diff --git a/doc/model.rst b/doc/model.rst new file mode 100644 index 000000000..62644b207 --- /dev/null +++ b/doc/model.rst @@ -0,0 +1,2270 @@ +***** +Model +***** +======== +Overview +======== +SmartSim ``Model`` objects enable users to execute computational tasks in an +``Experiment`` workflow, such as launching compiled applications, +running scripts, or performing general computational operations. A ``Model`` can be launched with +other SmartSim ``Models`` and ``Orchestrators`` to build AI-enabled workflows. +With the SmartSim ``Client`` (:ref:`SmartRedis`), data can be transferred out of the ``Model`` +into the ``Orchestrator`` for use in an ML Model (TF, TF-lite, PyTorch, or ONNX), online +training process, or additional ``Model`` applications. SmartSim ``Clients`` (SmartRedis) are available in +Python, C, C++, or Fortran. + +To initialize a SmartSim ``Model``, use the ``Experiment.create_model()`` API function. +When creating a ``Model``, a :ref:`RunSettings` object must be provided. A ``RunSettings`` +object specifies the ``Model`` executable (e.g. the full path to a compiled binary) as well as +executable arguments and launch parameters. These specifications include :ref:`launch` commands (e.g. `srun`, `aprun`, `mpiexec`, etc), +compute resource requirements, and application command-line arguments. + +Once a ``Model`` instance has been initialized, users have access to +the :ref:`Model API` functions to further configure the ``Model``. +The Model API functions provide users with the following capabilities: + +- :ref:`Attach Files to a SmartSim Model` +- :ref:`Colocate an Orchestrator to a SmartSim Model` +- :ref:`Attach a ML model to the SmartSim Model` +- :ref:`Attach a TorchScript function to the SmartSim Model` +- :ref:`Enable SmartSim Model data collision prevention` + +Once the ``Model`` has been configured and launched, a user can leverage an ``Orchestrator`` within a ``Model`` +through **two** strategies: + +- :ref:`Connect to a standalone Orchestrator` + When a ``Model`` is launched, it does not use or share compute + resources on the same host (computer/server) where a SmartSim ``Orchestrator`` is running. + Instead, it is launched on its own compute resources specified by the ``RunSettings`` object. + The ``Model`` can connect via a SmartSim ``Client`` to a launched standalone ``Orchestrator``. + +- :ref:`Connect to an colocated Orchestrator` + When the colocated ``Model`` is started, SmartSim launches an ``Orchestrator`` on the ``Model`` compute + nodes prior to the ``Model`` execution. The ``Model`` can then connect to the colocated ``Orchestrator`` + via a SmartSim ``Client``. + +For the client connection to be successful, the SmartSim ``Orchestrator`` must be launched +prior to the start of the ``Model``. + +.. note:: + A ``Model`` can be launched without an ``Orchestrator`` if data transfer and ML capabilities are not + required. + +SmartSim manages ``Model`` instances through the :ref:`Experiment API` by providing functions to +launch, monitor, and stop applications. Additionally, a ``Model`` can be launched individually +or as a group via an :ref:`Ensemble`. + +============== +Initialization +============== +Overview +======== +The :ref:`Experiment API` is responsible for initializing all workflow entities. +A ``Model`` is created using the ``Experiment.create_model()`` factory method, and users can customize the +``Model`` via the factory method parameters. + +The key initializer arguments are: + +- `name` (str): Specify the name of the ``Model`` for unique identification. +- `run_settings` (base.RunSettings): Describe execution settings for a ``Model``. +- `params` (t.Optional[t.Dict[str, t.Any]] = None): Provides a dictionary of parameters for ``Model``. +- `path` (t.Optional[str] = None): Path to where the ``Model`` should be executed at runtime. +- `enable_key_prefixing` (bool = False): Prefix the ``Model`` name to data sent to the ``Orchestrator`` to prevent key collisions. Default is `False`. +- `batch_settings` (t.Optional[base.BatchSettings] = None): Describes settings for batch workload treatment. + +A `name` and :ref:`RunSettings` reference are required to initialize a ``Model``. +Optionally, include a :ref:`BatchSettings` object to specify workload manager batch launching. + +.. note:: + ``BatchSettings`` attached to a ``Model`` are ignored when the ``Model`` is executed as part of an ensemble. + +The `params` factory method parameter for ``Model`` lets users define simulation parameters and their +values through a dictionary. Using :ref:`Model API` functions, users can write these parameters to +a file in the ``Model`` working directory. + +When a ``Model`` instance is passed to ``Experiment.generate()``, a +directory within the Experiment directory +is automatically created to store input and output files from the ``Model``. + +.. note:: + It is strongly recommended to invoke ``Experiment.generate()`` with the ``Model`` + instance before launching the ``Model``. If a path is not specified during + ``Experiment.create_model()``, calling ``Experiment.generate()`` with the ``Model`` + instance will result in SmartSim generating a ``Model`` directory within the + ``Experiment`` directory. This directory will be used to store the ``Model`` outputs + and attached files. + +.. _std_model_doc: +Example +======= +We provide a demonstration of how to initialize and launch a ``Model`` +within an ``Experiment`` workflow. All workflow entities are initialized through the +:ref:`Experiment API`. Consequently, initializing +a SmartSim ``Experiment`` is a prerequisite for ``Model`` initialization. + +To initialize an instance of the ``Experiment`` class, import the SmartSim ``Experiment`` module and invoke the ``Experiment`` constructor +with a `name` and `launcher`: + +.. code-block:: python + + from smartsim import Experiment + + # Init Experiment and specify to launch locally + exp = Experiment(name="getting-started", launcher="local") + +A ``Model`` requires ``RunSettings`` objects. We use the `exp` instance to +call the factory method ``Experiment.create_run_settings()`` to initialize a ``RunSettings`` +object. Finally, we specify the Python executable to run the script named +`script.py`: + +.. code-block:: python + + settings = exp.create_run_settings(exe="python", exe_args="script.py") + +We now have a ``RunSettings`` instance named `settings` that we can use to create +a ``Model`` instance that contains all of the information required to launch our application: + +.. code-block:: python + + model = exp.create_model(name="example-model", run_settings=settings) + +To create an isolated output directory for the ``Model``, invoke ``Experiment.generate()`` via the +``Experiment`` instance `exp` with `model` as an input parameter: + +.. code-block:: python + + model = exp.generate(model) + +Recall that all entities are launched, monitored and stopped by the ``Experiment`` instance. +To start ``Model``, invoke ``Experiment.start()`` via the ``Experiment`` instance `exp` with `model` as an +input parameter: + +.. code-block:: python + + exp.start(model) + +When the ``Experiment`` Python driver script is executed, two files from the ``Model`` will be created +in the Experiment working directory: + +1. `example-model.out` : this file will hold outputs produced by the ``Model`` workload +2. `example-model.err` : will hold any errors that occurred during ``Model`` execution + +.. _colo_model_doc: +====================== +Colocated Orchestrator +====================== +A SmartSim ``Model`` has the capability to share compute node(s) with a SmartSim ``Orchestrator`` in +a deployment known as a colocated ``Orchestrator``. In this scenario, the ``Orchestrator`` and ``Model`` share +compute resources. To achieve this, users need to initialize a ``Model`` instance using the +``Experiment.create_model()`` function, and then use one of the three functions listed below to +colocate an ``Orchestrator`` with the ``Model``. This ensures that SmartSim launches an ``Orchestrator`` +on the application compute node(s) before the ``Model`` execution. + +There are **three** different Model API functions to colocate a ``Model``: + +- ``Model.colocate_db_tcp()``: Colocate an ``Orchestrator`` instance and establish client communication using TCP/IP. +- ``Model.colocate_db_uds()``: Colocate an ``Orchestrator`` instance and establish client communication using Unix domain sockets (UDS). +- ``Model.colocate_db()``: (deprecated) An alias for `Model.colocate_db_tcp()`. + +Each function initializes an unsharded ``Orchestrator`` accessible only to the ``Model`` processes on the same compute node. When the ``Model`` +is started, the ``Orchestrator`` will be launched on the same compute resource as the ``Model``. Only the colocated ``Model`` +may communicate with the ``Orchestrator`` via a SmartRedis client by using the loopback TCP interface or +Unix Domain sockets. Extra parameters for the ``Orchestrator`` can be passed into the functions above +via `kwargs`. + +.. code-block:: python + + example_kwargs = { + "maxclients": 100000, + "threads_per_queue": 1, + "inter_op_threads": 1, + "intra_op_threads": 1 + } + +For a walkthrough of how to colocate a ``Model``, navigate to the :ref:`Colocated Orchestrator` for +instructions. + +.. _files_doc: +===== +Files +===== +Overview +======== +Applications often depend on external files (e.g. training datasets, evaluation datasets, etc) +to operate as intended. Users can instruct SmartSim to copy, symlink, or manipulate external files +prior to the ``Model`` launch via the ``Model.attach_generator_files()`` function. + +.. note:: + Multiple calls to ``Model.attach_generator_files()`` will overwrite previous file configurations + in the ``Model``. + +To attach a file to a ``Model`` for use at runtime, provide one of the following arguments to the +``Model.attach_generator_files()`` function: + +* `to_copy` (t.Optional[t.List[str]] = None): Files that are copied into the path of the entity. +* `to_symlink` (t.Optional[t.List[str]] = None): Files that are symlinked into the path of the entity. + +To specify a template file in order to programmatically replace specified parameters during generation +of the ``Model`` directory, pass the following value to the ``Model.attach_generator_files()`` function: + +* `to_configure` (t.Optional[t.List[str]] = None): Designed for text-based ``Model`` input files, + "to_configure" is exclusive to the ``Model``. During ``Model`` directory generation, the attached + files are parsed and specified tagged parameters are replaced with the `params` values that were + specified in the ``Experiment.create_model()`` factory method of the ``Model``. The default tag is a semicolon + (e.g., THERMO = ;THERMO;). + +In the :ref:`Example` subsection, we provide an example using the value `to_configure` +within ``attach_generator_files()``. + +.. _files_example_doc: +Example +======= +This example demonstrates how to attach a file to a ``Model`` for parameter replacement at time +of ``Model`` directory generation. This is accomplished using the `params` function parameter in +the ``Experiment.create_model()`` factory function and the `to_configure` function parameter +in ``Model.attach_generator_files()``. + +In this example, we have a text file named `params_inputs.txt`. Within the text, is the parameter `THERMO` +that is required by the application at runtime: + +.. code-block:: bash + + THERMO = ;THERMO; + +In order to have the tagged parameter `;THERMO;` replaced with a usable value at runtime, two steps are required: + +1. The `THERMO` variable must be included in ``Experiment.create_model()`` factory method as + part of the `params` parameter. +2. The file containing the tagged parameter `;THERMO;`, `params_inputs.txt`, must be attached to the ``Model`` + via the ``Model.attach_generator_files()`` method as part of the `to_configure` parameter. + +To encapsulate our application within a ``Model``, we must create an ``Experiment`` instance +to gain access to the ``Experiment`` factory method that creates the ``Model``. +Begin by importing the ``Experiment`` module, importing SmartSim `log` module and initializing +an ``Experiment``: + +.. code-block:: python + + from smartsim import Experiment + from smartsim.log import get_logger + + logger = get_logger("Experiment Log") + # Initialize the Experiment + exp = Experiment("getting-started", launcher="auto") + +A ``Model`` requires run settings. Create a simple ``RunSettings`` object to specify the path to +our application script as an executable argument and the executable to run the script: + +.. code-block:: python + + # Initialize a RunSettings object + model_settings = exp.create_run_settings(exe="python", exe_args="/path/to/application.py") + +Next, initialize a ``Model`` object with ``Experiment.create_model()`` +and pass in the `model_settings` instance: + +.. code-block:: python + + # Initialize a Model object + example_model = exp.create_model("model", model_settings, params={"THERMO":1}) + +We now have a ``Model`` instance named `example_model`. Attach the above text file +to the ``Model`` for use at entity runtime. To do so, we use the +``Model.attach_generator_files()`` function and specify the `to_configure` +parameter with the path to the text file, `params_inputs.txt`: + +.. code-block:: python + + # Attach the file to the Model instance + example_model.attach_generator_files(to_configure="path/to/params_inputs.txt") + +To created an isolated directory for the ``Model`` outputs and configuration files, invoke ``Experiment.generate()`` via the +``Experiment`` instance `exp` with `example_model` as an input parameter: + +.. code-block:: python + + exp.generate(example_model) + +After invoking ``Experiment.generate()``, the attached generator files will be available for the +application when ``exp.start(example_model)`` is called. + +The contents of `params_inputs.txt` after ``Model`` completion are: + +.. code-block:: bash + + THERMO = 1 + +====================== +Output and Error Files +====================== +By default, SmartSim stores the standard output and error of the ``Model`` in two files: + +* `.out` +* `.err` + +The files are created in the working directory of the ``Model``, and the filenames directly match the +``Model`` name. The `.out` file logs standard outputs and the +`.err` logs errors for debugging. + +.. note:: + Invoking ``Experiment.generate(model)`` will create a directory `model_name/` and will store + the two files within that directory. You can also specify a path for these files using the + `path` parameter when invoking ``Experiment.create_model()``. + +===================== +ML Models and Scripts +===================== +Overview +======== +SmartSim users have the capability to utilize ML runtimes within a ``Model``. +Functions accessible through a ``Model`` object support loading ML models (TensorFlow, TensorFlow-lite, +PyTorch, and ONNX) and TorchScripts into standalone ``Orchestrators`` or colocated ``Orchestrators`` at +application runtime. + +Users can follow **two** processes to load a ML model to the ``Orchestrator``: + +- :ref:`from memory` +- :ref:`from file` + +Users can follow **three** processes to load a TorchScript to the ``Orchestrator``: + +- :ref:`from memory` +- :ref:`from file` +- :ref:`from string` + +Once a ML model or TorchScript is loaded into the ``Orchestrator``, ``Model`` objects can +leverage ML capabilities by utilizing the SmartSim client (:ref:`SmartRedis`) +to execute the stored ML models or TorchScripts. + +.. _ai_model_doc: +AI Models +========= +When configuring a ``Model``, users can instruct SmartSim to load +Machine Learning (ML) models dynamically to the ``Orchestrator`` (colocated or standalone). ML models added +are loaded into the ``Orchestrator`` prior to the execution of the ``Model``. To load an ML model +to the ``Orchestrator``, SmartSim users can provide the ML model **in-memory** or specify the **file path** +when using the ``Model.add_ml_model()`` function. The supported ML frameworks are TensorFlow, +TensorFlow-lite, PyTorch, and ONNX. + +When attaching an ML model using ``Model.add_ml_model()``, the +following arguments are offered to customize the storage and execution of the ML model: + +- `name` (str): name to reference the model in the ``Orchestrator``. +- `backend` (str): name of the backend (TORCH, TF, TFLITE, ONNX). +- `model` (t.Optional[str] = None): A ML model in memory (only supported for non-colocated ``Orchestrators``). +- `model_path` (t.Optional[str] = None): serialized ML model. +- `device` (t.Literal["CPU", "GPU"] = "CPU"): name of device for execution, defaults to “CPU”. +- `devices_per_node` (int = 1): 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 CPU. +- `first_device` (int = 0): 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. +- `batch_size` (int = 0): batch size for execution, defaults to 0. +- `min_batch_size` (int = 0): minimum batch size for ML model execution, defaults to 0. +- `min_batch_timeout` (int = 0): time to wait for minimum batch size, defaults to 0. +- `tag` (str = ""): additional tag for ML model information, defaults to “”. +- `inputs` (t.Optional[t.List[str]] = None): ML model inputs (TF only), defaults to None. +- `outputs` (t.Optional[t.List[str]] = None): ML model outputs (TF only), defaults to None. + +.. _in_mem_ML_model_ex: +------------------------------------- +Example: Attach an in-memory ML Model +------------------------------------- +This example demonstrates how to attach an in-memory ML model to a SmartSim ``Model`` +to load into an ``Orchestrator`` at ``Model`` runtime. + +.. note:: + This example assumes: + + - an ``Orchestrator`` is launched prior to the ``Model`` execution + - an initialized ``Model`` named `smartsim_model` exists within the ``Experiment`` workflow + +**Define an in-memory Keras CNN** + +The ML model must be defined using one of the supported ML frameworks. For the purpose of the example, +we define a Keras CNN in the same script as the SmartSim ``Experiment``: + +.. code-block:: python + + def create_tf_cnn(): + """Create an in-memory Keras CNN for example purposes + + """ + from smartsim.ml.tf import serialize_model + n = Net() + input_shape = (3,3,1) + inputs = Input(input_shape) + outputs = n(inputs) + model = keras.Model(inputs=inputs, outputs=outputs, name=n.name) + + return serialize_model(model) + + # Get and save TF model + model, inputs, outputs = create_tf_cnn() + +**Attach the ML model to a SmartSim Model** + +Assuming an initialized ``Model`` named `smartsim_model` exists, we add the in-memory TensorFlow model using +the ``Model.add_ml_model()`` function and specify the in-memory ML model to the parameter `model`: + +.. code-block:: python + + smartsim_model.add_ml_model(name="cnn", backend="TF", model=model, device="GPU", devices_per_node=2, first_device=0, inputs=inputs, outputs=outputs) + +In the above ``smartsim_model.add_ml_model()`` code snippet, we offer the following arguments: + +- `name` ("cnn"): A name to reference the ML model in the ``Orchestrator``. +- `backend` ("TF"): Indicating that the ML model is a TensorFlow model. +- `model` (model): The in-memory representation of the TensorFlow model. +- `device` ("GPU"): Specifying the device for ML model execution. +- `devices_per_node` (2): Use two GPUs per node. +- `first_device` (0): Start with 0 index GPU. +- `inputs` (inputs): The name of the ML model input nodes (TensorFlow only). +- `outputs` (outputs): The name of the ML model output nodes (TensorFlow only). + +.. warning:: + Calling `exp.start(smartsim_model)` prior to instantiation of an ``Orchestrator`` will result in + a failed attempt to load the ML model to a non-existent ``Orchestrator``. + +When the ``Model`` is started via ``Experiment.start()``, the ML model will be loaded to the +launched ``Orchestrator``. The ML model can then be executed on the ``Orchestrator`` via a SmartSim +client (:ref:`SmartRedis`) within the application code. + +.. _from_file_ML_model_ex: +---------------------------------------- +Example: Attaching an ML Model from file +---------------------------------------- +This example demonstrates how to attach a ML model from file to a SmartSim ``Model`` +to load into an ``Orchestrator`` at ``Model`` runtime. + +.. note:: + This example assumes: + + - a standalone ``Orchestrator`` is launched prior to the ``Model`` execution + - an initialized ``Model`` named `smartsim_model` exists within the ``Experiment`` workflow + +**Define a Keras CNN script** + +The ML model must be defined using one of the supported ML frameworks. For the purpose of the example, +we define the function `save_tf_cnn()` that saves a Keras CNN to a file named `model.pb` located in our +Experiment path: + +.. code-block:: python + + def save_tf_cnn(path, file_name): + """Create a Keras CNN and save to file for example purposes""" + from smartsim.ml.tf import freeze_model + + n = Net() + input_shape = (3, 3, 1) + n.build(input_shape=(None, *input_shape)) + inputs = Input(input_shape) + outputs = n(inputs) + model = keras.Model(inputs=inputs, outputs=outputs, name=n.name) + + return freeze_model(model, path, file_name) + + # Get and save TF model + model_file, inputs, outputs = save_tf_cnn(model_dir, "model.pb") + +**Attach the ML model to a SmartSim Model** + +Assuming an initialized ``Model`` named `smartsim_model` exists, we add a TensorFlow model using +the ``Model.add_ml_model()`` function and specify the ML model path to the parameter `model_path`: + +.. code-block:: python + + smartsim_model.add_ml_model(name="cnn", backend="TF", model_path=model_file, device="GPU", devices_per_node=2, first_device=0, inputs=inputs, outputs=outputs) + +In the above ``smartsim_model.add_ml_model()`` code snippet, we offer the following arguments: + +- `name` ("cnn"): A name to reference the ML model in the ``Orchestrator``. +- `backend` ("TF"): Indicating that the ML model is a TensorFlow model. +- `model_path` (model_file): The path to the ML model script. +- `device` ("GPU"): Specifying the device for ML model execution. +- `devices_per_node` (2): Use two GPUs per node. +- `first_device` (0): Start with 0 index GPU. +- `inputs` (inputs): The name of the ML model input nodes (TensorFlow only). +- `outputs` (outputs): The name of the ML model output nodes (TensorFlow only). + +.. warning:: + Calling `exp.start(smartsim_model)` prior to instantiation of an ``Orchestrator`` will result in + a failed attempt to load the ML model to a non-existent ``Orchestrator``. + +When the ``Model`` is started via ``Experiment.start()``, the ML model will be loaded to the +launched ``Orchestrator``. The ML model can then be executed on the ``Orchestrator`` via a SmartSim +client (:ref:`SmartRedis`) within the application code. + +.. _TS_doc: +TorchScripts +============ +When configuring a ``Model``, users can instruct SmartSim to load TorchScripts dynamically +to the ``Orchestrator``. TorchScripts added are loaded into the ``Orchestrator`` prior to +the execution of the ``Model``. To load a TorchScript to the ``Orchestrator``, SmartSim users +can follow one of the processes: + +- :ref:`Define a TorchScript function in-memory` + Use the ``Model.add_function()`` to instruct SmartSim to load an in-memory TorchScript to the ``Orchestrator``. +- :ref:`Define a TorchScript function from file` + Provide file path to ``Model.add_script()`` to instruct SmartSim to load the TorchScript from file to the ``Orchestrator``. +- :ref:`Define a TorchScript function as string` + Provide function string to ``Model.add_script()`` to instruct SmartSim to load a raw string as a TorchScript function to the ``Orchestrator``. + +Continue or select the respective process link to learn more on how each function (``Model.add_script()`` and ``Model.add_function()``) +dynamically loads TorchScripts to the ``Orchestrator``. + +.. _in_mem_TF_doc: +------------------------------- +Attach an in-memory TorchScript +------------------------------- +Users can define TorchScript functions within the Python driver script +to attach to a ``Model``. This feature is supported by ``Model.add_function()`` which provides flexible +device selection, allowing users to choose between which device the the TorchScript is executed on, `"GPU"` or `"CPU"`. +In environments with multiple devices, specific device numbers can be specified using the +`devices_per_node` parameter. + +.. warning:: + ``Model.add_function()`` does **not** support loading in-memory TorchScript functions to a colocated ``Orchestrator``. + If you would like to load a TorchScript function to a colocated ``Orchestrator``, define the function + as a :ref:`raw string` or :ref:`load from file`. + +When specifying an in-memory TF function using ``Model.add_function()``, the +following arguments are offered: + +- `name` (str): reference name for the script inside of the ``Orchestrator``. +- `function` (t.Optional[str] = None): TorchScript function code. +- `device` (t.Literal["CPU", "GPU"] = "CPU"): device for script execution, defaults to “CPU”. +- `devices_per_node` (int = 1): 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 CPU. +- `first_device` (int = 0): 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. + +.. _in_mem_TF_ex: +Example: Loading a in-memory TorchScript function +------------------------------------------------- +This example walks through the steps of instructing SmartSim to load an in-memory TorchScript function +to a standalone ``Orchestrator``. + +.. note:: + The example assumes: + + - a standalone ``Orchestrator`` is launched prior to the ``Model`` execution + - an initialized ``Model`` named `smartsim_model` exists within the ``Experiment`` workflow + +**Define an in-memory TF function** + +To begin, define an in-memory TorchScript function within the Python driver script. +For the purpose of the example, we add a simple TorchScript function, `timestwo`: + +.. code-block:: python + + def timestwo(x): + return 2*x + +**Attach the in-memory TorchScript function to a SmartSim Model** + +We use the ``Model.add_function()`` function to instruct SmartSim to load the TorchScript function `timestwo` +onto the launched standalone ``Orchestrator``. Specify the function `timestwo` to the `function` +parameter: + +.. code-block:: python + + smartsim_model.add_function(name="example_func", function=timestwo, device="GPU", devices_per_node=2, first_device=0) + +In the above ``smartsim_model.add_function()`` code snippet, we offer the following arguments: + +- `name` ("example_func"): A name to uniquely identify the ML model within the ``Orchestrator``. +- `function` (timestwo): Name of the TorchScript function defined in the Python driver script. +- `device` ("CPU"): Specifying the device for ML model execution. +- `devices_per_node` (2): Use two GPUs per node. +- `first_device` (0): Start with 0 index GPU. + +.. warning:: + Calling `exp.start(smartsim_model)` prior to instantiation of an ``Orchestrator`` will result in + a failed attempt to load the ML model to a non-existent ``Orchestrator``. + +When the ``Model`` is started via ``Experiment.start()``, the TF function will be loaded to the +standalone ``Orchestrator``. The function can then be executed on the ``Orchestrator`` via a SmartSim +client (:ref:`SmartRedis`) within the application code. + +.. _TS_from_file: +------------------------------ +Attach a TorchScript from file +------------------------------ +Users can attach TorchScript functions from a file to a ``Model`` and upload them to a +colocated or standalone ``Orchestrator``. This functionality is supported by the ``Model.add_script()`` +function's `script_path` parameter. The function supports +flexible device selection, allowing users to choose between `"GPU"` or `"CPU"` via the `device` parameter. +In environments with multiple devices, specific device numbers can be specified using the +`devices_per_node` parameter. + +When specifying a TorchScript using ``Model.add_script()``, the +following arguments are offered: + +- `name` (str): Reference name for the script inside of the ``Orchestrator``. +- `script` (t.Optional[str] = None): String of function code (e.g. TorchScript code string). +- `script_path` (t.Optional[str] = None): path to TorchScript code. +- `device` (t.Literal["CPU", "GPU"] = "CPU"): device for script execution, defaults to “CPU”. +- `devices_per_node` (int = 1): 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 CPU. +- `first_device` (int = 0): 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. + +.. _TS_from_file_ex: +Example: Loading a TorchScript from File +---------------------------------------- +This example walks through the steps of instructing SmartSim to load a TorchScript from file +to a ``Orchestrator``. + +.. note:: + This example assumes: + + - a ``Orchestrator`` is launched prior to the ``Model`` execution + - an initialized ``Model`` named `smartsim_model` exists within the ``Experiment`` workflow + +**Define a TorchScript script** + +For the example, we create the Python script `torchscript.py`. The file contains a +simple torch function shown below: + +.. code-block:: python + + def negate(x): + return torch.neg(x) + +**Attach the TorchScript script to a SmartSim Model** + +Assuming an initialized ``Model`` named `smartsim_model` exists, we add a TorchScript script using +the ``Model.add_script()`` function and specify the script path to the parameter `script_path`: + +.. code-block:: python + + smartsim_model.add_script(name="example_script", script_path="path/to/torchscript.py", device="GPU", devices_per_node=2, first_device=0) + +In the above ``smartsim_model.add_script()`` code snippet, we offer the following arguments: + +- `name` ("example_script"): Reference name for the script inside of the ``Orchestrator``. +- `script_path` ("path/to/torchscript.py"): Path to the script file. +- `device` ("CPU"): device for script execution. +- `devices_per_node` (2): Use two GPUs per node. +- `first_device` (0): Start with 0 index GPU. + +.. warning:: + Calling `exp.start(smartsim_model)` prior to instantiation of an ``Orchestrator`` will result in + a failed attempt to load the ML model to a non-existent ``Orchestrator``. + +When `smartsim_model` is started via ``Experiment.start()``, the TorchScript will be loaded from file to the +``Orchestrator`` that is launched prior to the start of the `smartsim_model`. + +.. _TS_raw_string: +--------------------------------- +Define TorchScripts as raw string +--------------------------------- +Users can upload TorchScript functions from string to send to a colocated or +standalone ``Orchestrator``. This feature is supported by the +``Model.add_script()`` function's `script` parameter. The function supports +flexible device selection, allowing users to choose between `"GPU"` or `"CPU"` via the `device` parameter. +In environments with multiple devices, specific device numbers can be specified using the +`devices_per_node` parameter. + +When specifying a TorchScript using ``Model.add_script()``, the +following arguments are offered: + +- `name` (str): Reference name for the script inside of the ``Orchestrator``. +- `script` (t.Optional[str] = None): String of function code (e.g. TorchScript code string). +- `script_path` (t.Optional[str] = None): path to TorchScript code. +- `device` (t.Literal["CPU", "GPU"] = "CPU"): device for script execution, defaults to “CPU”. +- `devices_per_node` (int = 1): 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 CPU. +- `first_device` (int = 0): 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. + +.. _TS_from_file_ex: +Example: Loading a TorchScript from string +------------------------------------------ +This example walks through the steps of instructing SmartSim to load a TorchScript function +from string to a ``Orchestrator`` before the execution of the associated ``Model``. + +.. note:: + This example assumes: + + - a ``Orchestrator`` is launched prior to the ``Model`` execution + - an initialized ``Model`` named `smartsim_model` exists within the ``Experiment`` workflow + +**Define a string TorchScript** + +Define the TorchScript code as a variable in the Python driver script: + +.. code-block:: python + + torch_script_str = "def negate(x):\n\treturn torch.neg(x)\n" + +**Attach the TorchScript function to a SmartSim Model** + +Assuming an initialized ``Model`` named `smartsim_model` exists, we add a TensorFlow model using +the ``Model.add_script()`` function and specify the variable `torch_script_str` to the parameter +`script`: + +.. code-block:: python + + smartsim_model.add_script(name="example_script", script=torch_script_str, device="GPU", devices_per_node=2, first_device=0) + +In the above ``smartsim_model.add_script()`` code snippet, we offer the following arguments: + +- `name` ("example_script"): key to store script under. +- `script` (torch_script_str): TorchScript code. +- `device` ("CPU"): device for script execution. +- `devices_per_node` (2): Use two GPUs per node. +- `first_device` (0): Start with 0 index GPU. + +.. warning:: + Calling `exp.start(smartsim_model)` prior to instantiation of an ``Orchestrator`` will result in + a failed attempt to load the ML model to a non-existent ``Orchestrator``. + +When the ``Model`` is started via ``Experiment.start()``, the TorchScript will be loaded to the +``Orchestrator`` that is launched prior to the start of the ``Model``. + +.. _model_key_collision: +========================= +Data Collision Prevention +========================= +Overview +======== +If an ``Experiment`` consists of multiple ``Models`` that attempt to use the same code to access their respective +data in the ``Orchestrator``, the names used to reference data, ML models, and scripts will be +identical, and without the use of SmartSim and SmartRedis helper methods, ``Models`` +will end up inadvertently accessing or overwriting each other’s data. To prevent this +situation, the SmartSim ``Model`` object supports key prefixing, which automatically prepends +the name of the ``Model`` to the keys it uses to access data. With this enabled, collision is +avoided and ``Models`` can use the same code. + +For example, assume you have two ``Models`` in an ``Experiment``, named `model_0` and `model_1`. In each +application code you use the function ``Client.put_tensor("tensor_0")``. With ``Model`` key prefixing +turned on, the `model_0` and `model_1` ``Model`` applications can access the tensor `"tensor_0"` by name without +overwriting or accessing the other ``Model`` `"tensor_0"` tensor. + +Enabling and Disabling +====================== +SmartSim provides support for prefixing on a ``Model`` for tensors, ``Datasets``, lists, ML models, and scripts. +The key components of prefixing functionality include: + +1. **Sending Data to the Orchestrator**: Users can send data to an ``Orchestrator`` + with the ``Model`` `name` prepended to the data `name`. +2. **Retrieving Data from the Orchestrator**: Users can instruct a ``Client`` to prepend a + ``Model`` `name` to a key during data retrieval, polling, or check for existence on the ``Orchestrator``. + +To enable prefixing on the ``Model``, users should utilize the ``Model.enable_key_prefixing()`` function +in the driver script. This function automatically activates prefixing for tensors, ``Datasets``, +and lists. Additionally, users can control prefixing for each data structure through ``Client`` +functions in the ``Model`` script: + +- Tensor: ``Client.use_tensor_ensemble_prefix()`` +- ``Dataset``: ``Client.use_dataset_ensemble_prefix()`` +- Aggregation lists: ``Client.use_list_ensemble_prefix()`` + +.. note:: + ML model and script prefixing is not automatically enabled through ``Model.enable_key_prefixing()``. + +.. warning:: + To access the ``Client`` prefixing functions (e.g. ``Client.use_tensor_ensemble_prefix()``, + etc.), prefixing must be enabled on the ``Model`` through ``Model.enable_key_prefixing()``. + +Users can manage prefixing for ML models and scripts using the ``Client.enable_model_ensemble_prefix()`` +function in the application. However, prior to using this function, users must enable prefixing on the ``Model`` through +``Model.enable_key_prefixing()``. + +For examples on sending prefixed data to the ``Orchestrator``, read the +:ref:`put/set operations` section. + +Users can instruct SmartSim to prepend a ``Model`` `name` when searching for data in the +``Orchestrator``. This is achieved through the ``Client.set_data_source()`` function in the ``Model`` +application. To implement this functionality: + +1. Use ``Model.register_incoming_entity()`` on the ``Model`` intending to search for data in the ``Orchestrator``. +2. Pass the SmartSim entity (e.g., another ``Model``) to ``Model.register_incoming_entity()`` in order to + reference the ``Model`` prefix in the application code. +3. In the application, instruct the ``Client`` to prepend the specified ``Model`` `name` during key searches + using ``Client.set_data_source("model_name")``. + +For examples on instructing a ``Client`` to append a ``Model`` `name` to a key when searching for data, read the +:ref:`get operations` section, :ref:`run operations` section, or :ref:`copy/rename/delete +operations` section. + +.. _put_set_prefix: +Put/Set Operations +================== +In the following tabs we provide snippets of driver script and application code to demonstrate +activating and deactivating prefixing for tensors, ``Datasets``, lists, ML models and scripts using +SmartRedis put/get semantics. + +.. tabs:: + + .. group-tab:: Tensor + **Activate Tensor Prefixing in the Driver Script** + + To activate prefixing on a ``Model`` in the driver script, a user must use the function + ``Model.enable_key_prefixing()``. This functionality ensures that the ``Model`` `name` + is prepended to each tensor `name` sent to the ``Orchestrator`` from within the ``Model`` + executable code. + + In the driver script snippet below, we take an initialized ``Model`` and activate tensor + prefixing through the ``enable_key_prefixing()`` function: + + .. code-block:: python + + # Create the run settings for the Model + model_settings = exp.create_run_settings(exe=exe_ex, exe_args="/path/to/application_script.py") + + # Create a Model instance named 'model' + model = exp.create_model("model_name", model_settings) + # Enable tensor prefixing on the 'model' instance + model.enable_key_prefixing() + + In the `model` application, two tensors named `tensor_1` and `tensor_2` are sent to a launched ``Orchestrator``. + The contents of the ``Orchestrator`` after ``Model`` completion are: + + .. code-block:: bash + + 1) "model_name.tensor_1" + 2) "model_name.tensor_2" + + You will notice that the ``Model`` name `model_name` has been prepended to each tensor `name` + and stored in the ``Orchestrator``. + + **Activate Tensor Prefixing in the Application** + + Users can further configure tensor prefixing in the application by using + the ``Client`` function ``use_tensor_ensemble_prefix()``. By specifying a boolean + value to the function, users can turn prefixing on and off. + + .. note:: + To have access to ``Client.use_tensor_ensemble_prefix()``, prefixing must be enabled + on the ``Model`` in the driver script via ``Model.enable_key_prefixing()``. + + In the application snippet below, we demonstrate enabling and disabling tensor prefixing: + + .. code-block:: python + + # Disable key prefixing + client.use_tensor_ensemble_prefix(False) + # Place a tensor in the Orchestrator + client.put_tensor("tensor_1", np.array([1, 2, 3, 4])) + # Enable key prefixing + client.use_tensor_ensemble_prefix(True) + # Place a tensor in the Orchestrator + client.put_tensor("tensor_2", np.array([5, 6, 7, 8])) + + In the application, two tensors named `tensor_1` and `tensor_2` are sent to a launched ``Orchestrator``. + The contents of the ``Orchestrator`` after ``Model`` completion are: + + .. code-block:: bash + + 1) "tensor_1" + 2) "model_name.tensor_2" + + You will notice that the ``Model`` name `model_name` is **not** prefixed to `tensor_1` since + we disabled tensor prefixing before sending the tensor to the ``Orchestrator``. However, + when we enabled tensor prefixing and sent the second tensor, the ``Model`` name was prefixed + to `tensor_2`. + + .. group-tab:: Dataset + **Activate Dataset Prefixing in the Driver Script** + + To activate prefixing on a ``Model`` in the driver script, a user must use the function + ``Model.enable_key_prefixing()``. This functionality ensures that the ``Model`` `name` + is prepended to each ``Dataset`` `name` sent to the ``Orchestrator`` from within the ``Model``. + + In the driver script snippet below, we take an initialized ``Model`` and activate ``Dataset`` + prefixing through the ``enable_key_prefixing()`` function: + + .. code-block:: python + + # Create the run settings for the Model + model_settings = exp.create_run_settings(exe=exe_ex, exe_args="/path/to/application_script.py") + + # Create a Model instance named 'model' + model = exp.create_model("model_name", model_settings) + # Enable Dataset prefixing on the 'model' instance + model.enable_key_prefixing() + + In the `model` application, two Datasets named `dataset_1` and `dataset_2` are sent to a launched ``Orchestrator``. + The contents of the ``Orchestrator`` after ``Model`` completion are: + + .. code-block:: bash + + 1) "model_name.{dataset_1}.dataset_tensor_1" + 2) "model_name.{dataset_1}.meta" + 3) "model_name.{dataset_2}.dataset_tensor_2" + 4) "model_name.{dataset_2}.meta" + + You will notice that the ``Model`` name `model_name` has been prefixed to each ``Dataset`` `name` + and stored in the ``Orchestrator``. + + **Activate Dataset Prefixing in the Application** + + Users can further configure ``Dataset`` prefixing in the application by using + the ``Client`` function ``use_dataset_ensemble_prefix()``. By specifying a boolean + value to the function, users can turn prefixing on and off. + + .. note:: + To have access to ``Client.use_dataset_ensemble_prefix()``, prefixing must be enabled + on the ``Model`` in the driver script via ``Model.enable_key_prefixing()``. + + In the application snippet below, we demonstrate enabling and disabling ``Dataset`` prefixing: + + .. code-block:: python + + # Disable key prefixing + client.use_dataset_ensemble_prefix(False) + # Place a Dataset in the Orchestrator + client.put_dataset(dataset_1) + # Enable key prefixing + client.use_dataset_ensemble_prefix(True) + # Place a Dataset in the Orchestrator + client.put_dataset(dataset_2) + + In the application, we have two ``Datasets`` named `dataset_1` and `dataset_2`. + We then send them to a launched ``Orchestrator``. The contents of the ``Orchestrator`` after ``Model`` completion are: + + .. code-block:: bash + + 1) "{dataset_1}.dataset_tensor_1" + 2) "{dataset_1}.meta" + 3) "model_name.{dataset_2}.dataset_tensor_1" + 4) "model_name.{dataset_2}.meta" + + You will notice that the ``Model`` name `model_name` is **not** prefixed to `dataset_1` since + we disabled ``Dataset`` prefixing before sending the ``Dataset`` to the ``Orchestrator``. However, + when we enabled ``Dataset`` prefixing and sent the second ``Dataset``, the ``Model`` name was prefixed + to `dataset_2`. + + .. group-tab:: Aggregation List + **Activate Aggregation List Prefixing in the Driver Script** + + To activate prefixing on a ``Model`` in the driver script, a user must use the function + ``Model.enable_key_prefixing()``. This functionality ensures that the ``Model`` `name` + is prepended to each list `name` sent to the ``Orchestrator`` from within the ``Model``. + + In the driver script snippet below, we take an initialized ``Model`` and activate list + prefixing through the ``enable_key_prefixing()`` function: + + .. code-block:: python + + # Create the run settings for the Model + model_settings = exp.create_run_settings(exe=exe_ex, exe_args="/path/to/application_script.py") + + # Create a Model instance named 'model' + model = exp.create_model("model_name", model_settings) + # Enable list prefixing on the 'model' instance + model.enable_key_prefixing() + + In the `model` application, a list named `dataset_list` is sent to a launched ``Orchestrator``. + The contents of the ``Orchestrator`` after ``Model`` completion are: + + .. code-block:: bash + + 1) "model_name.dataset_list" + + You will notice that the ``Model`` name `model_name` has been prefixed to the list `name` + and stored in the ``Orchestrator``. + + **Activate Aggregation List Prefixing in the Application** + + Users can further configure list prefixing in the application by using + the ``Client`` function ``use_list_ensemble_prefix()``. By specifying a boolean + value to the function, users can turn prefixing on and off. + + .. note:: + To have access to ``Client.use_list_ensemble_prefix()``, prefixing must be enabled + on the ``Model`` in the driver script via ``Model.enable_key_prefixing()``. + + In the application snippet below, we demonstrate enabling and disabling list prefixing: + + .. code-block:: python + + # Disable key prefixing + client.use_list_ensemble_prefix(False) + # Place a Dataset in the Orchestrator + client.put_dataset(dataset_1) + # Place a list in the Orchestrator + client.append_to_list("list_1", dataset_1) + # Enable key prefixing + client.use_dataset_ensemble_prefix(True) + # Place a Dataset in the Orchestrator + client.put_dataset(dataset_2) + # Append Dataset to list in the Orchestrator + client.append_to_list("list_2", dataset_2) + + In the application, two lists named `list_1` and `list_2` are sent to the ``Orchestrator``. + The contents of the ``Orchestrator`` after ``Model`` completion are: + + .. code-block:: bash + + 1) "list_1" + 2) "model_name.{dataset_1}.meta" + 3) "model_name.{dataset_1}.dataset_tensor_1" + 4) "model_name.list_2" + 5) "model_name.{dataset_2}.meta" + 6) "model_name.{dataset_2}.dataset_tensor_2" + + You will notice that the ``Model`` name `model_name` is **not** prefixed to `list_1` since + we disabled list prefixing before sending the list to the ``Orchestrator``. However, + when we enabled list prefixing and sent the second list, the ``Model`` name was prefixed + to `list_2` as well as the list ``Dataset`` members. + + .. note:: + The ``Datasets`` sent to the ``Orchestrator`` are all prefixed. This is because + ``Model.enable_key_prefixing()`` turns on prefixing for tensors, ``Datasets`` and lists. + + .. group-tab:: ML Model + **Activate ML model Prefixing in the Application** + + Users can configure ML model prefixing in the application by using + the ``Client`` function ``use_model_ensemble_prefix()``. By specifying a boolean + value to the function, users can turn prefixing on and off. + + .. note:: + To have access to ``Client.use_model_ensemble_prefix()``, prefixing must be enabled + on the ``Model`` in the driver script via ``Model.enable_key_prefixing()``. + + In the application snippet below, we demonstrate enabling and disabling ML model prefixing: + + .. code-block:: python + + # Disable ML model prefixing + client.use_model_ensemble_prefix(False) + # Send ML model to the Orchestrator + client.set_model( + "ml_model_1", serialized_model_1, "TF", device="CPU", inputs=inputs, outputs=outputs + ) + # Enable ML model prefixing + client.use_model_ensemble_prefix(True) + # Send prefixed ML model to the Orchestrator + client.set_model( + "ml_model_2", serialized_model_2, "TF", device="CPU", inputs=inputs, outputs=outputs + ) + + In the application, two ML models named `ml_model_1` and `ml_model_2` are sent + to a launched ``Orchestrator``. The contents of the ``Orchestrator`` after ``Model`` completion are: + + .. code-block:: bash + + 1) "ml_model_1" + 2) "model_name.ml_model_2" + + You will notice that the ``Model`` name `model_name` is **not** prefixed to `ml_model_1` since + we disabled ML model prefixing before sending the ML model to the ``Orchestrator``. However, + when we enabled ML model prefixing and sent the second ML model, the ``Model`` name was prefixed + to `ml_model_2`. + + .. group-tab:: Script + **Activate Script Prefixing in the Application** + + Users can configure script prefixing in the application by using + the ``Client`` function ``use_model_ensemble_prefix()``. By specifying a boolean + value to the function, users can turn prefixing on and off. + + .. note:: + To have access to ``Client.use_model_ensemble_prefix()``, prefixing must be enabled + on the ``Model`` in the driver script via ``Model.enable_key_prefixing()``. + + In the application snippet below, we demonstrate enabling and disabling script prefixing: + + .. code-block:: python + + # Disable script prefixing + client.use_model_ensemble_prefix(False) + # Store a script in the Orchestrator + client.set_function("script_1", script_1) + # Enable script prefixing + client.use_model_ensemble_prefix(True) + # Store a prefixed script in the Orchestrator + client.set_function("script_2", script_2) + + In the application, two ML models named `script_1` and `script_2` are sent + to a launched ``Orchestrator``. The contents of the ``Orchestrator`` after ``Model`` completion are: + + .. code-block:: bash + + 1) "script_1" + 2) "model_name.script_2" + + You will notice that the ``Model`` name `model_name` is **not** prefixed to `script_1` since + we disabled script prefixing before sending the script to the ``Orchestrator``. However, + when we enabled script prefixing and sent the second script, the ``Model`` name was prefixed + to `script_2`. + +.. _get_prefix: + +Get Operations +============== +In the following sections, we walk through snippets of application code to demonstrate the retrieval +of prefixed tensors, ``Datasets``, lists, ML models, and scripts using SmartRedis put/get +semantics. The examples demonstrate retrieval within the same application where the data +structures were placed, as well as scenarios where data structures are placed by separate +applications. + +.. tabs:: + + .. group-tab:: Tensor + **Retrieve A Tensor Placed By The Same Application** + + SmartSim supports retrieving prefixed tensors sent to the ``Orchestrator`` from within the + same application where the tensor was placed. To achieve this, users must + provide the ``Model`` `name` that stored the tensor to ``Client.set_data_source()``. This action + instructs the ``Client`` to prepend the ``Model`` name to all key searches. For SmartSim to + recognize the ``Model`` `name` as a data source, users must execute the + ``Model.register_incoming_entity()`` function on the ``Model`` and pass the self ``Model`` `name` + in the driver script. + + As an example, we placed a prefixed tensor on the ``Orchestrator`` within a ``Model`` named + `model_1`. The ``Orchestrator`` contents are: + + .. code-block:: bash + + 1) "model_1.tensor_name" + + .. note:: + In the driver script, after initializing the ``Model`` instance named `model_1`, + we execute ``model_1.register_incoming_entity(model_1)``. By passing the ``Model`` + instance to itself, we instruct SmartSim to recognize the name of `model_1` as a valid data + source for subsequent use in ``Client.set_data_source()``. + + In the application snippet below, we demonstrate retrieving the tensor: + + .. code-block:: python + + # Set the name to prepend to key searches + client.set_data_source("model_1") + # Retrieve the prefixed tensor + tensor_data = client.get_tensor("tensor_name") + # Log the tensor data + client.log_data(LLInfo, f"The tensor value is: {tensor_data}") + + In the `model.out` file, the ``Client`` will log the message:: + Default@00-00-00:The tensor value is: [1 2 3 4] + + **Retrieve A Tensor Placed By An Alternate Application** + + SmartSim supports retrieving prefixed tensors sent to the ``Orchestrator`` by separate + ``Models``. To achieve this, users need to provide the ``Model`` `name` that stored the tensor + to ``Client.set_data_source()``. This action instructs the ``Client`` to prepend the ``Model`` + `name` to all key searches. For SmartSim to recognize the ``Model`` `name` as a data source, + users must execute the ``Model.register_incoming_entity()`` function on the ``Model`` + responsible for the search and pass the ``Model`` instance that stored the data in the + driver script. + + In the example, a ``Model`` named `model_1` has placed a tensor in a standalone + ``Orchestrator`` with prefixing enabled on the ``Model``. The contents of the ``Orchestrator`` + are as follows: + + .. code-block:: bash + + 1) "model_1.tensor_name" + + We create a separate ``Model``, named `model_2`, with the executable application code below. + + .. note:: + In the driver script, after initializing the ``Model`` instance named `model_2`, + we execute ``model_2.register_incoming_entity(model_1)``. By passing the producer ``Model`` + instance to the consumer ``Model``, we instruct SmartSim to recognize the name of `model_1` as a valid data + source for subsequent use in ``Client.set_data_source()``. + + Here we retrieve the stored tensor named `tensor_name`: + + .. code-block:: python + + # Set the Model source name + client.set_data_source("model_1") + # Retrieve the prefixed tensor + tensor_data = client.get_tensor("tensor_name") + # Log the tensor data + client.log_data(LLInfo, f"The tensor value is: {tensor_data}") + + In the `model.out` file, the ``Client`` will log the message:: + Default@00-00-00:The tensor value is: [1 2 3 4] + + .. group-tab:: Dataset + **Retrieve A Dataset Placed By The Same Application** + + SmartSim supports retrieving prefixed ``Datasets`` sent to the ``Orchestrator`` from within the + same application where the ``Dataset`` was placed. To achieve this, users must + provide the ``Model`` `name` that stored the ``Dataset`` to ``Client.set_data_source()``. This action + instructs the ``Client`` to prepend the ``Model`` name to all key searches. For SmartSim to + recognize the ``Model`` `name` as a data source, users must execute the + ``Model.register_incoming_entity()`` function on the ``Model`` and pass the self ``Model`` `name`. + + As an example, we placed a prefixed ``Dataset`` on the ``Orchestrator`` within a ``Model`` named + `model_1`. The ``Orchestrator`` contents are: + + .. code-block:: bash + + 1) "model_1.{dataset_name}.dataset_tensor" + 2) "model_1.{dataset_name}.meta" + + .. note:: + In the driver script, after initializing the ``Model`` instance named `model_1`, + we execute ``model_1.register_incoming_entity(model_1)``. By passing the ``Model`` + instance to itself, we instruct SmartSim to recognize the name of `model_1` as a valid data + source for subsequent use in ``Client.set_data_source()``. + + In the application snippet below, we demonstrate retrieving the ``Dataset``: + + .. code-block:: python + + # Set the name to prepend to key searches + client.set_data_source("model_1") + # Retrieve the prefixed Dataset + dataset_data = client.get_dataset("dataset_name") + # Log the Dataset data + client.log_data(LLInfo, f"The Dataset value is: {dataset_data}") + + In the `model.out` file, the ``Client`` will log the message: + + .. code-block:: bash + + Default@00-00-00:Default@00-00-00:The dataset value is: + + DataSet (dataset_name): + Tensors: + dataset_tensor: + type: 16 bit unsigned integer + dimensions: [4] + elements: 4 + Metadata: + none + + **Retrieve A Dataset Placed By An Alternate Application** + + SmartSim supports retrieving prefixed ``Datasets`` sent to the ``Orchestrator`` by separate + ``Models``. To achieve this, users need to provide the ``Model`` `name` that stored the ``Dataset`` + to ``Client.set_data_source()``. This action instructs the ``Client`` to prepend the ``Model`` + `name` to all key searches. For SmartSim to recognize the ``Model`` `name` as a data source, + users must execute the ``Model.register_incoming_entity()`` function on the ``Model`` + responsible for the search and pass the ``Model`` instance that stored the data. + + In the example, a ``Model`` named `model_1` has placed a ``Dataset`` in a standalone + ``Orchestrator`` with prefixing enabled on the ``Model``. The contents of the ``Orchestrator`` + are as follows: + + .. code-block:: bash + + 1) "model_1.{dataset_name}.dataset_tensor" + 2) "model_1.{dataset_name}.meta" + + We create a separate ``Model``, named `model_2`, with the executable application code below. + + .. note:: + In the driver script, after initializing the ``Model`` instance named `model_2`, + we execute ``model_2.register_incoming_entity(model_1)``. By passing the producer ``Model`` + instance to the consumer ``Model``, we instruct SmartSim to recognize the name of `model_1` as a valid data + source for subsequent use in ``Client.set_data_source()``. + + Here we retrieve the stored ``Dataset`` named `dataset_name`: + + .. code-block:: python + + # Set the Model source name + client.set_data_source("model_1") + # Retrieve the prefixed Dataset + dataset_data = client.get_dataset("dataset_name") + # Log the Dataset data + client.log_data(LLInfo, f"The Dataset value is: {dataset_data}") + + In the `model.out` file, the ``Client`` will log the message: + + .. code-block:: bash + + Default@00-00-00:Default@00-00-00:The Dataset value is: + + DataSet (dataset_name): + Tensors: + dataset_tensor: + type: 16 bit unsigned integer + dimensions: [4] + elements: 4 + Metadata: + none + + .. group-tab:: Aggregation List + **Retrieve A Aggregation List Placed By The Same Application** + + SmartSim supports retrieving prefixed lists sent to the ``Orchestrator`` from within the + same application where the list was placed. To achieve this, users must + provide the ``Model`` `name` that stored the list to ``Client.set_data_source()``. This action + instructs the ``Client`` to prepend the ``Model`` name to all key searches. For SmartSim to + recognize the ``Model`` `name` as a data source, users must execute the + ``Model.register_incoming_entity()`` function on the ``Model`` and pass the self ``Model`` `name`. + + As an example, we placed a prefixed list on the ``Orchestrator`` within a ``Model`` named + `model_1`. The ``Orchestrator`` contents are: + + .. code-block:: bash + + 1) "model_1.dataset_list" + + .. note:: + In the driver script, after initializing the ``Model`` instance named `model_1`, + we execute ``model_1.register_incoming_entity(model_1)``. By passing the ``Model`` + instance to itself, we instruct SmartSim to recognize the name of `model_1` as a valid data + source for subsequent use in ``Client.set_data_source()``. + + In the application snippet below, we demonstrate checking the length of the list: + + .. code-block:: python + + # Set the name to prepend to key searches + client.set_data_source("model_1") + # Retrieve the prefixed list + list_data = client.get_datasets_from_list("dataset_list") + # Log the list data + client.log_data(LLInfo, f"The length of the list is: {len(list_data)}") + + In the `model.out` file, the ``Client`` will log the message:: + The length of the list is: 1 + + **Retrieve A Aggregation List Placed By An Alternate Application** + + SmartSim supports retrieving prefixed lists sent to the ``Orchestrator`` by separate + ``Models``. To achieve this, users need to provide the ``Model`` `name` that stored the list + to ``Client.set_data_source()``. This action instructs the ``Client`` to prepend the ``Model`` + `name` to all key searches. For SmartSim to recognize the ``Model`` `name` as a data source, + users must execute the ``Model.register_incoming_entity()`` function on the ``Model`` + responsible for the search and pass the ``Model`` instance that stored the data. + + In the example, a ``Model`` named `model_1` has placed a list in a standalone + ``Orchestrator`` with prefixing enabled on the ``Model``. The contents of the ``Orchestrator`` + are as follows: + + .. code-block:: bash + + 1) "model_name.dataset_list" + + We create a separate ``Model``, named `model_2`, with the executable application code below. + + .. note:: + In the driver script, after initializing the ``Model`` instance named `model_2`, + we execute ``model_2.register_incoming_entity(model_1)``. By passing the producer ``Model`` + instance to the consumer ``Model``, we instruct SmartSim to recognize the name of `model_1` as a valid data + source for subsequent use in ``Client.set_data_source()``. + + Here we check the length of the list named `dataset_list`: + + .. code-block:: python + + # Set the Model source name + client.set_data_source("model_1") + # Retrieve the prefixed list + list_data = client.get_datasets_from_list("dataset_list") + # Log the list data + client.log_data(LLInfo, f"The length of the list is: {len(list_data)}") + + In the `model.out` file, the ``Client`` will log the message:: + The length of the list is: 1 + + .. group-tab:: ML Model + **Retrieve A ML Model Placed By The Same Application** + + SmartSim supports retrieving prefixed ML models sent to the ``Orchestrator`` from within the + same application where the ML model was placed. To achieve this, users must + provide the ``Model`` `name` that stored the ML model to ``Client.set_data_source()``. This action + instructs the ``Client`` to prepend the ``Model`` name to all key searches. For SmartSim to + recognize the ``Model`` `name` as a data source, users must execute the + ``Model.register_incoming_entity()`` function on the ``Model`` and pass the self ``Model`` `name`. + + As an example, we placed a prefixed ML model on the ``Orchestrator`` within a ``Model`` named + `model_1`. The ``Orchestrator`` contents are: + + .. code-block:: bash + + 1) "model_1.mnist_cnn" + + .. note:: + In the driver script, after initializing the ``Model`` instance named `model_1`, + we execute ``model_1.register_incoming_entity(model_1)``. By passing the ``Model`` + instance to itself, we instruct SmartSim to recognize the name of `model_1` as a valid data + source for subsequent use in ``Client.set_data_source()``. + + In the application snippet below, we demonstrate retrieving the ML model: + + .. code-block:: python + + # Set the name to prepend to key searches + client.set_data_source("model_1") + # Retrieve the prefixed ML model + model_data = client.get_model("mnist_cnn") + + **Retrieve A ML Model Placed By An Alternate Application** + + SmartSim supports retrieving prefixed ML model sent to the ``Orchestrator`` by separate + ``Models``. To achieve this, users need to provide the ``Model`` `name` that stored the ML model + to ``Client.set_data_source()``. This action instructs the ``Client`` to prepend the ``Model`` + `name` to all key searches. For SmartSim to recognize the ``Model`` `name` as a data source, + users must execute the ``Model.register_incoming_entity()`` function on the ``Model`` + responsible for the search and pass the ``Model`` instance that stored the data. + + In the example, a ``Model`` named `model_1` has placed a ML model in a standalone + ``Orchestrator`` with prefixing enabled on the ``Model``. The contents of the ``Orchestrator`` + are as follows: + + .. code-block:: bash + + 1) "model_1.mnist_cnn" + + We create a separate ``Model``, named `model_2`, with the executable application code below. + + .. note:: + In the driver script, after initializing the ``Model`` instance named `model_2`, + we execute ``model_2.register_incoming_entity(model_1)``. By passing the producer ``Model`` + instance to the consumer ``Model``, we instruct SmartSim to recognize the name of `model_1` as a valid data + source for subsequent use in ``Client.set_data_source()``. + + Here we retrieve the stored ML model named `mnist_cnn`: + + .. code-block:: python + + # Set the Model source name + client.set_data_source("model_1") + # Retrieve the prefixed model + model_data = client.get_model("mnist_cnn") + + .. group-tab:: Script + **Retrieve A Script Placed By The Same Application** + + SmartSim supports retrieving prefixed scripts sent to the ``Orchestrator`` from within the + same application where the script was placed. To achieve this, users must + provide the ``Model`` `name` that stored the script to ``Client.set_data_source()``. This action + instructs the ``Client`` to prepend the ``Model`` name to all key searches. For SmartSim to + recognize the ``Model`` `name` as a data source, users must execute the + ``Model.register_incoming_entity()`` function on the ``Model`` and pass the self ``Model`` `name`. + + As an example, we placed a prefixed script on the ``Orchestrator`` within a ``Model`` named + `model_1`. The ``Orchestrator`` contents are: + + .. code-block:: bash + + 1) "model_1.normalizer" + + .. note:: + In the driver script, after initializing the ``Model`` instance named `model_1`, + we execute ``model_1.register_incoming_entity(model_1)``. By passing the ``Model`` + instance to itself, we instruct SmartSim to recognize the name of `model_1` as a valid data + source for subsequent use in ``Client.set_data_source()``. + + In the application snippet below, we demonstrate retrieving the script: + + .. code-block:: python + + # Set the name to prepend to key searches + client.set_data_source("model_1") + # Retrieve the prefixed script + script_data = client.get_script("normalizer") + # Log the script data + client.log_data(LLInfo, f"The script data is: {script_data}") + + In the `model.out` file, the ``Client`` will log the message: + + .. code-block:: bash + + The script data is: def normalize(X): + """Simple function to normalize a tensor""" + mean = X.mean() + std = X.std() + + return (X-mean)/std + + **Retrieve A Script Placed By An Alternate Application** + + SmartSim supports retrieving prefixed scripts sent to the ``Orchestrator`` by separate + ``Models``. To achieve this, users need to provide the ``Model`` `name` that stored the script + to ``Client.set_data_source()``. This action instructs the ``Client`` to prepend the ``Model`` + `name` to all key searches. For SmartSim to recognize the ``Model`` `name` as a data source, + users must execute the ``Model.register_incoming_entity()`` function on the ``Model`` + responsible for the search and pass the ``Model`` instance that stored the data. + + In the example, a ``Model`` named `model_1` has placed a script in a standalone + ``Orchestrator`` with prefixing enabled on the ``Model``. The contents of the ``Orchestrator`` + are as follows: + + .. code-block:: bash + + 1) "model_1.normalizer" + + We create a separate ``Model``, named `model_2`, with the executable application code below. + + .. note:: + In the driver script, after initializing the ``Model`` instance named `model_2`, + we execute ``model_2.register_incoming_entity(model_1)``. By passing the producer ``Model`` + instance to the consumer ``Model``, we instruct SmartSim to recognize the name of `model_1` as a valid data + source for subsequent use in ``Client.set_data_source()``. + + Here we retrieve the stored script named `normalizer`: + + .. code-block:: python + + # Set the Model source name + client.set_data_source("model_1") + # Retrieve the prefixed script + script_data = client.get_script("model_1.normalizer") + # Log the script data + client.log_data(LLInfo, f"The script data is: {script_data}") + + In the `model.out` file, the ``Client`` will log the message: + + .. code-block:: bash + + The script data is: def normalize(X): + """Simple function to normalize a tensor""" + mean = X.mean() + std = X.std() + + return (X-mean)/std + +.. _run_prefix: +Run Operations +============== +In the following sections, we walk through snippets of application code to demonstrate executing +prefixed ML models and scripts using SmartRedis run semantics. The examples demonstrate +executing within the same application where the ML Model and Script were placed, as well as scenarios +where ML Model and Script are placed by separate applications. + +.. tabs:: + + .. group-tab:: ML Model + **Access ML Models From The Application They Were Loaded In** + + SmartSim supports executing prefixed ML models with prefixed tensors sent to the ``Orchestrator`` from within + the same application that the ML model was placed. To achieve this, users must + provide the ``Model`` `name` that stored the ML model and input tensors to ``Client.set_data_source()``. This action + instructs the ``Client`` to prepend the ``Model`` name to all key names. For SmartSim to + recognize the ``Model`` `name` as a data source, users must execute the + ``Model.register_incoming_entity()`` function on the ``Model`` and pass the self ``Model`` `name`. + + As an example, we placed a prefixed ML model and tensor on the ``Orchestrator`` within a ``Model`` named + `model_1`. The ``Orchestrator`` contents are: + + .. code-block:: bash + + 1) "model_1.mnist_cnn" + 2) "model_1.mnist_images" + + .. note:: + In the driver script, after initializing the ``Model`` instance named `model_1`, + we execute ``model_1.register_incoming_entity(model_1)``. By passing the ``Model`` + instance to itself, we instruct SmartSim to recognize the name of `model_1` as a valid data + source for subsequent use in ``Client.set_data_source()``. + + In the application snippet below, we demonstrate running the ML model: + + .. code-block:: python + + # Set the Model source name + client.set_data_source("model_1") + # Run the ML model + client.run_model(name="mnist_cnn", inputs=["mnist_images"], outputs=["Identity"]) + + The ``Orchestrator`` now contains prefixed output tensors: + + .. code-block:: bash + + 1) "model_1.Identity" + 2) "model_1.mnist_cnn" + 3) "model_1.mnist_images" + + .. note:: + The output tensors are prefixed because we executed ``model_1.enable_key_prefixing()`` + in the driver script which enables prefixing for tensors, ``Datasets`` and lists. + + **Access ML Models From Outside The Application They Were Loaded In** + + SmartSim supports executing prefixed ML models with prefixed tensors sent to the ``Orchestrator`` by separate + ``Models``. To achieve this, users need to provide the ``Model`` `name` that stored the ML model and tensor + to ``Client.set_data_source()``. This action instructs the ``Client`` to prepend the ``Model`` + `name` to all key searches. For SmartSim to recognize the ``Model`` `name` as a data source, + users must execute the ``Model.register_incoming_entity()`` function on the ``Model`` + responsible for the search and pass the ``Model`` instance that stored the data. + + In the example, a ``Model`` named `model_1` has placed a ML model and tensor in a standalone + ``Orchestrator`` with prefixing enabled on the ``Model``. The contents of the ``Orchestrator`` + are as follows: + + .. code-block:: bash + + 1) "model_1.mnist_cnn" + 2) "model_1.mnist_images" + + We create a separate ``Model``, named `model_2`, with the executable application code below. + + .. note:: + In the driver script, after initializing the ``Model`` instance named `model_2`, + we execute ``model_2.register_incoming_entity(model_1)``. By passing the producer ``Model`` + instance to the consumer ``Model``, we instruct SmartSim to recognize the name of `model_1` as a valid data + source for subsequent use in ``Client.set_data_source()``. + + In the application snippet below, we demonstrate running the ML model: + + .. code-block:: python + + # Set the Model source name + client.set_data_source("model_1") + # Run the ML model + client.run_model(name="mnist_cnn", inputs=["mnist_images"], outputs=["Identity"]) + + The ``Orchestrator`` now contains prefixed output tensors: + + .. code-block:: bash + + 1) "model_2.Identity" + 2) "model_1.mnist_cnn" + 3) "model_1.mnist_images" + + .. note:: + The output tensors are prefixed because we executed ``model_2.enable_key_prefixing()`` + in the driver script which enables prefixing for tensors, ``Datasets`` and lists. + + .. group-tab:: Script + + **Access Scripts From The Application They Were Loaded In** + + SmartSim supports executing prefixed scripts with prefixed tensors sent to the ``Orchestrator`` from within + the same application that the script was placed. To achieve this, users must + provide the ``Model`` `name` that stored the script and input tensors to ``Client.set_data_source()``. This action + instructs the ``Client`` to prepend the ``Model`` name to all key names. For SmartSim to + recognize the ``Model`` `name` as a data source, users must execute the + ``Model.register_incoming_entity()`` function on the ``Model`` and pass the self ``Model`` `name`. + + As an example, we placed a prefixed script and tensor on the ``Orchestrator`` within a ``Model`` named + `model_1`. The ``Orchestrator`` contents are: + + .. code-block:: bash + + 1) "model_1.normalizer" + 2) "model_1.X_rand" + + To run the script, the prefixed script name `"model_name.normalizer"` and prefixed + input tensors `"model_name.X_rand"` must be provided, as demonstrated below: + + .. code-block:: python + + # Set the Model source name + client.set_data_source("model_1") + # Run the script + client.run_script("normalizer", "normalize", inputs=["X_rand"], outputs=["X_norm"]) + + The ``Orchestrator`` now contains prefixed output tensors: + + .. code-block:: bash + + 1) "model_1.normalizer" + 2) "model_1.X_rand" + 3) "model_1.X_norm" + + .. note:: + The output tensors are prefixed because we executed ``model_1.enable_key_prefixing()`` + in the driver script which enables prefixing for tensors, ``Datasets`` and lists. + + **Access Scripts From Outside The Application They Were Loaded In** + + SmartSim supports executing prefixed scripts with prefixed tensors sent to the ``Orchestrator`` by separate + ``Models``. To achieve this, users need to provide the ``Model`` `name` that stored the script and tensor + to ``Client.set_data_source()``. This action instructs the ``Client`` to prepend the ``Model`` + `name` to all key searches. For SmartSim to recognize the ``Model`` `name` as a data source, + users must execute the ``Model.register_incoming_entity()`` function on the ``Model`` + responsible for the search and pass the ``Model`` instance that stored the data. + + In the example, a ``Model`` named `model_1` has placed a script and tensor in a standalone + ``Orchestrator`` with prefixing enabled on the ``Model``. The contents of the ``Orchestrator`` + are as follows: + + .. code-block:: bash + + 1) "model_1.normalizer" + 2) "model_1.X_rand" + + We create a separate ``Model``, named `model_2`, with the executable application code below. + + .. note:: + In the driver script, after initializing the ``Model`` instance named `model_2`, + we execute ``model_2.register_incoming_entity(model_1)``. By passing the producer ``Model`` + instance to the consumer ``Model``, we instruct SmartSim to recognize the name of `model_1` as a valid data + source for use in ``Client.set_data_source()``. + + In the application snippet below, we demonstrate running the script: + + .. code-block:: python + + # Set the Model source name + client.set_data_source("model_1") + # Run the script + client.run_script("normalizer", "normalize", inputs=["X_rand"], outputs=["X_norm"]) + + The ``Orchestrator`` now contains prefixed output tensors: + + .. code-block:: bash + + 1) "model_1.normalizer" + 2) "model_1.X_rand" + 3) "model_2.X_norm" + + .. note:: + The output tensors are prefixed because we executed ``model_2.enable_key_prefixing()`` + in the driver script which enables prefixing for tensors, ``Datasets`` and lists. + +.. _copy_rename_del_prefix: +Copy/Rename/Delete Operations +============================= +In the following sections, we walk through snippets of application code to demonstrate the copy, rename and delete +operations on prefixed tensors, ``Datasets``, lists, ML models, and scripts. The examples +demonstrate these operations within the same script where the data +structures were placed, as well as scenarios where data structures are placed by separate +scripts. + +.. tabs:: + + .. group-tab:: Tensor + **Copy/Rename/Delete Operations On Tensors In The Same Application** + + SmartSim supports copy/rename/delete operations on prefixed tensors sent to the ``Orchestrator`` from within + the same application that the tensor was placed. To achieve this, users must + provide the ``Model`` `name` that stored the tensor to ``Client.set_data_source()``. This action + instructs the ``Client`` to prepend the ``Model`` name to all key names. For SmartSim to + recognize the ``Model`` `name` as a data source, users must execute the + ``Model.register_incoming_entity()`` function on the ``Model`` and pass the self ``Model`` `name`. + + As an example, we placed a prefixed tensor on the ``Orchestrator`` within a ``Model`` named + `model_1`. The ``Orchestrator`` contents are: + + .. code-block:: bash + + 1) "model_1.tensor" + + .. note:: + In the driver script, after initializing the ``Model`` instance named `model_1`, + we execute ``model_1.register_incoming_entity(model_1)``. By passing the ``Model`` + instance to itself, we instruct SmartSim to recognize the name of `model_1` as a valid data + source for subsequent use in ``Client.set_data_source()``. + + To rename the tensor in the ``Orchestrator``, we provide self ``Model`` `name` + to ``Client.set_data_source()`` then execute the function ``rename_tensor()``: + + .. code-block:: python + + # Set the Model source name + client.set_data_source("model_1") + # Rename the tensor + client.rename_tensor("tensor", "renamed_tensor") + + Because prefixing is enabled on the ``Model`` via ``enable_key_prefixing()`` in the driver script, + SmartSim will keep the prefix on the tensor but replace the tensor name as shown in the ``Orchestrator``: + + .. code-block:: bash + + 1) "model_1.renamed_tensor" + + Next, we copy the prefixed tensor to a new destination: + + .. code-block:: python + + client.copy_tensor("renamed_tensor", "copied_tensor") + + Since tensor prefixing is enabled on the ``Client``, the `copied_tensor` is prefixed: + + .. code-block:: bash + + 1) "model_1.renamed_tensor" + 2) "model_1.copied_tensor" + + Next, delete `renamed_tensor`: + + .. code-block:: python + + client.delete_tensor("renamed_tensor") + + The contents of the ``Orchestrator`` are: + + .. code-block:: bash + + 1) "model_1.copied_tensor" + + **Copy/Rename/Delete Operations On Tensors Placed By An Alternate Application** + + SmartSim supports copy/rename/delete operations on prefixed tensors sent to the ``Orchestrator`` by separate + ``Models``. To achieve this, users need to provide the ``Model`` `name` that stored the tensor + to ``Client.set_data_source()``. This action instructs the ``Client`` to prepend the ``Model`` + `name` to all key searches. For SmartSim to recognize the ``Model`` `name` as a data source, + users must execute the ``Model.register_incoming_entity()`` function on the ``Model`` + responsible for the search and pass the ``Model`` instance that stored the data. + + In the example, a ``Model`` named `model_1` has placed a tensor in a standalone ``Orchestrator`` with prefixing enabled + on the ``Client``. The ``Orchestrator`` contents are: + + .. code-block:: bash + + 1) "model_1.tensor" + + .. note:: + In the driver script, after initializing the ``Model`` instance named `model_2`, + we execute ``model_2.register_incoming_entity(model_1)``. By passing the producer ``Model`` + instance to the consumer ``Model``, we instruct SmartSim to recognize the name of `model_1` as a valid data + source for subsequent use in ``Client.set_data_source()``. + + From within a separate ``Model`` named `model_2`, we perform basic copy/rename/delete operations. + To instruct the ``Client`` to prepend a ``Model`` name to all key searches, use the + ``Client.set_data_source()`` function. Specify the ``Model`` name `model_1` + that placed the tensor in the ``Orchestrator``: + + .. code-block:: python + + # Set the Model source name + client.set_data_source("model_1") + + To rename the tensor in the ``Orchestrator``, we provide the tensor `name`: + + .. code-block:: python + + client.rename_tensor("tensor", "renamed_tensor") + + SmartSim will replace the prefix with the current ``Model`` name since prefixing is enabled + on the current ``Model``. The contents of the ``Orchestrator`` are: + + .. code-block:: bash + + 1) "model_2.renamed_tensor" + + .. note:: + In the driver script, we also register `model_2` as an entity on itself via ``model_2.register_incoming_entity(model_2)``. + This way we can use ``Client.set_data_source()`` to search for data placed by `model_2`. + + Next, we copy the prefixed tensor to a new destination: + + .. code-block:: python + + # Set the Model source name + client.set_data_source("model_2") + # Copy the tensor data + client.copy_tensor("renamed_tensor", "copied_tensor") + + The ``Orchestrator`` contents are: + + .. code-block:: bash + + 1) "model_2.renamed_tensor" + 2) "model_2.copied_tensor" + + Next, delete `copied_tensor` by specifying the name: + + .. code-block:: python + + client.delete_tensor("copied_tensor") + + The contents of the ``Orchestrator`` are: + + .. code-block:: bash + + 1) "model_2.renamed_tensor" + + .. group-tab:: Dataset + **Copy/Rename/Delete Operations On A Dataset In The Same Application** + + SmartSim supports copy/rename/delete operations on prefixed ``Datasets`` sent to the ``Orchestrator`` from within + the same application that the ``Dataset`` was placed. To achieve this, users must + provide the ``Model`` `name` that stored the ``Dataset`` to ``Client.set_data_source()``. This action + instructs the ``Client`` to prepend the ``Model`` name to all key names. For SmartSim to + recognize the ``Model`` `name` as a data source, users must execute the + ``Model.register_incoming_entity()`` function on the ``Model`` and pass the self ``Model`` `name`. + + As an example, we placed a prefixed ``Dataset`` on the ``Orchestrator`` within a ``Model`` named + `model_1`. The ``Orchestrator`` contents are: + + .. code-block:: bash + + 1) "model_1.{dataset}.dataset_tensor" + 2) "model_1.{dataset}.meta" + + .. note:: + In the driver script, after initializing the ``Model`` instance named `model_1`, + we execute ``model_1.register_incoming_entity(model_1)``. By passing the ``Model`` + instance to itself, we instruct SmartSim to recognize the name of `model_1` as a valid data + source for subsequent use in ``Client.set_data_source()``. + + To rename the ``Dataset`` in the ``Orchestrator``, we provide self ``Model`` `name` + to ``Client.set_data_source()`` then execute the function ``rename_tensor()``: + + .. code-block:: python + + # Set the Model source name + client.set_data_source("model_1") + # Rename the Dataset + client.rename_dataset("dataset", "renamed_dataset") + + Because prefixing is enabled on the ``Model`` via ``enable_key_prefixing()`` in the driver script, + SmartSim will keep the prefix on the ``Dataset`` but replace the ``Dataset`` name as shown in the ``Orchestrator``: + + .. code-block:: bash + + 1) "model_1.{renamed_dataset}.dataset_tensor" + 2) "model_1.{renamed_dataset}.meta" + + Next, we copy the prefixed ``Dataset`` to a new destination: + + .. code-block:: python + + client.copy_dataset("renamed_dataset", "copied_dataset") + + Since ``Dataset`` prefixing is enabled on the ``Client``, the `copied_dataset` is prefixed: + + .. code-block:: bash + + 1) "model_1.{renamed_dataset}.dataset_tensor" + 2) "model_1.{renamed_dataset}.meta" + 3) "model_1.{copied_dataset}.dataset_tensor" + 4) "model_1.{copied_dataset}.meta" + + Next, delete `copied_dataset`: + + .. code-block:: python + + client.delete_dataset("model_name.copied_dataset") + + The contents of the ``Orchestrator`` are: + + .. code-block:: bash + + 1) "model_1.{renamed_dataset}.dataset_tensor" + 2) "model_1.{renamed_dataset}.meta" + + **Copy/Rename/Delete Operations On Datasets Placed By An Alternate Application** + + SmartSim supports copy/rename/delete operations on prefixed ``Datasets`` sent to the ``Orchestrator`` by separate + ``Models``. To achieve this, users need to provide the ``Model`` `name` that stored the ``Dataset`` + to ``Client.set_data_source()``. This action instructs the ``Client`` to prepend the ``Model`` + `name` to all key searches. For SmartSim to recognize the ``Model`` `name` as a data source, + users must execute the ``Model.register_incoming_entity()`` function on the ``Model`` + responsible for the search and pass the ``Model`` instance that stored the data. + + In the example, a ``Model`` named `model_1` has placed a ``Dataset`` in a standalone ``Orchestrator`` with prefixing enabled + on the ``Client``. The ``Orchestrator`` contents are: + + .. code-block:: bash + + 1) "model_1.{dataset}.dataset_tensor" + 2) "model_1.{dataset}.meta" + + .. note:: + In the driver script, after initializing the ``Model`` instance named `model_2`, + we execute ``model_2.register_incoming_entity(model_1)``. By passing the producer ``Model`` + instance to the consumer ``Model``, we instruct SmartSim to recognize the name of `model_1` as a valid data + source for subsequent use in ``Client.set_data_source()``. + + From within a separate ``Model`` named `model_2`, we perform basic copy/rename/delete operations. + To instruct the ``Client`` to prepend a ``Model`` name to all key searches, use the + ``Client.set_data_source()`` function. Specify the ``Model`` name `model_1` + that placed the ``Dataset`` in the ``Orchestrator``: + + .. code-block:: python + + client.set_data_source("model_1") + + To rename the ``Dataset`` in the ``Orchestrator``, we provide the ``Dataset`` `name`: + + .. code-block:: python + + client.rename_tensor("dataset", "renamed_dataset") + + SmartSim will replace the prefix with the current ``Model`` name since prefixing is enabled + on the current ``Model`` via ``Model.enable_key_prefixing()`` in the driver script. + The contents of the ``Orchestrator`` are: + + .. code-block:: bash + + 1) "model_2.{renamed_dataset}.dataset_tensor" + 2) "model_2.{renamed_dataset}.meta" + + .. note:: + In the driver script, we also register `model_2` as an entity on itself via ``model_2.register_incoming_entity(model_2)``. + This way we can use ``Client.set_data_source()`` to search for data placed by `model_2`. + + Next, we copy the prefixed ``Dataset`` to a new destination: + + .. code-block:: python + + # Set the Model source name + client.set_data_source("model_2") + # Copy the tensor data + client.copy_dataset("renamed_dataset", "copied_dataset") + + The ``Orchestrator`` contents are: + + .. code-block:: bash + + 1) "model_2.{renamed_dataset}.dataset_tensor" + 2) "model_2.{renamed_dataset}.meta" + 3) "model_2.{copied_dataset}.dataset_tensor" + 4) "model_2.{copied_dataset}.meta" + + Next, delete `copied_dataset` by specifying the name: + + .. code-block:: python + + client.delete_dataset("copied_tensor") + + The contents of the ``Orchestrator`` are: + + .. code-block:: bash + + 1) "model_2.{renamed_dataset}.dataset_tensor" + 2) "model_2.{renamed_dataset}.meta" + + .. group-tab:: Aggregation List + **Copy/Rename/Delete Operations On A Aggregation List In The Same Application** + + SmartSim supports copy/rename/delete operations on prefixed lists sent to the ``Orchestrator`` from within + the same application that the list was placed. To achieve this, users must + provide the ``Model`` `name` that stored the list to ``Client.set_data_source()``. This action + instructs the ``Client`` to prepend the ``Model`` name to all key names. For SmartSim to + recognize the ``Model`` `name` as a data source, users must execute the + ``Model.register_incoming_entity()`` function on the ``Model`` and pass the self ``Model`` `name`. + + As an example, we placed a prefixed list on the ``Orchestrator`` within a ``Model`` named + `model_1`. The ``Orchestrator`` contents are: + + .. code-block:: bash + + 1) "model_1.list_of_datasets" + + .. note:: + In the driver script, after initializing the ``Model`` instance named `model_1`, + we execute ``model_1.register_incoming_entity(model_1)``. By passing the ``Model`` + instance to itself, we instruct SmartSim to recognize the name of `model_1` as a valid data + source for subsequent use in ``Client.set_data_source()``. + + To rename the list in the ``Orchestrator``, we provide self ``Model`` `name` + to ``Client.set_data_source()`` then execute the function ``rename_list()``: + + .. code-block:: python + + # Set the Model source name + client.set_data_source("model_1") + # Rename the list + client.rename_list("list_of_datasets", "renamed_list") + + Because prefixing is enabled on the ``Model`` via ``enable_key_prefixing()`` in the driver script, + SmartSim will keep the prefix on the list but replace the list name as shown in the ``Orchestrator``: + + .. code-block:: bash + + 1) "model_1.renamed_list" + + Next, we copy the prefixed list to a new destination: + + .. code-block:: python + + client.copy_list("renamed_list", "copied_list") + + Since list prefixing is enabled on the ``Client``, the `copied_list` is prefixed: + + .. code-block:: bash + + 1) "model_1.renamed_list" + 2) "model_1.copied_list" + + Next, delete `copied_list`: + + .. code-block:: python + + client.delete_list("copied_list") + + The contents of the ``Orchestrator`` are: + + .. code-block:: bash + + 1) "model_1.renamed_list" + + **Copy/Rename/Delete Operations On Aggregation Lists Placed By An Alternate Application** + + SmartSim supports copy/rename/delete operations on prefixed lists sent to the ``Orchestrator`` by separate + ``Models``. To achieve this, users need to provide the ``Model`` `name` that stored the list + to ``Client.set_data_source()``. This action instructs the ``Client`` to prepend the ``Model`` + `name` to all key searches. For SmartSim to recognize the ``Model`` `name` as a data source, + users must execute the ``Model.register_incoming_entity()`` function on the ``Model`` + responsible for the search and pass the ``Model`` instance that stored the data. + + In the example, a ``Model`` named `model_1` has placed a list in a standalone ``Orchestrator`` with prefixing enabled + on the ``Client``. The ``Orchestrator`` contents are: + + .. code-block:: bash + + 1) "model_1.list_of_datasets" + + .. note:: + In the driver script, after initializing the ``Model`` instance named `model_2`, + we execute ``model_2.register_incoming_entity(model_1)``. By passing the producer ``Model`` + instance to the consumer ``Model``, we instruct SmartSim to recognize the name of `model_1` as a valid data + source for subsequent use in ``Client.set_data_source()``. + + From within a separate ``Model`` named `model_2`, we perform basic copy/rename/delete operations. + To instruct the ``Client`` to prepend a ``Model`` name to all key searches, use the + ``Client.set_data_source()`` function. Specify the ``Model`` name `model_1` + that placed the list in the ``Orchestrator``: + + .. code-block:: python + + client.set_data_source("model_1") + + To rename the list in the ``Orchestrator``, we provide the list `name`: + + .. code-block:: python + + client.rename_list("list_of_datasets", "renamed_list") + + SmartSim will replace the prefix with the current ``Model`` name since prefixing is enabled + on the current ``Model``. The contents of the ``Orchestrator`` are: + + .. code-block:: bash + + 1) "model_2.renamed_list" + + .. note:: + In the driver script, we also register `model_2` as an entity on itself via ``model_2.register_incoming_entity(model_2)``. + This way we can use ``Client.set_data_source()`` to search for data placed by `model_2`. + + Next, we copy the prefixed list to a new destination: + + .. code-block:: python + + # Set the Model source name + client.set_data_source("model_2") + # Copy the tensor data + client.copy_dataset("renamed_list", "copied_list") + + The ``Orchestrator`` contents are: + + .. code-block:: bash + + 1) "model_2.renamed_list" + 2) "model_2.copied_list" + + Next, delete `copied_list` by specifying the name: + + .. code-block:: python + + client.delete_list("copied_list") + + The contents of the ``Orchestrator`` are: + + .. code-block:: bash + + 1) "model_2.renamed_list" + + .. group-tab:: ML Model + **Delete ML Models From The Application They Were Loaded In** + + SmartSim supports delete operations on prefixed ML models sent to the ``Orchestrator`` from within + the same application that the ML model was placed. To achieve this, users must + provide the ``Model`` `name` that stored the ML model to ``Client.set_data_source()``. This action + instructs the ``Client`` to prepend the ``Model`` name to all key names. For SmartSim to + recognize the ``Model`` `name` as a data source, users must execute the + ``Model.register_incoming_entity()`` function on the ``Model`` and pass the self ``Model`` `name`. + + As an example, we placed a prefixed ML model on the ``Orchestrator`` within a ``Model`` named + `model_1`. The ``Orchestrator`` contents are: + + .. note:: + In the driver script, after initializing the ``Model`` instance named `model_1`, + we execute ``model_1.register_incoming_entity(model_1)``. By passing the ``Model`` + instance to itself, we instruct SmartSim to recognize the name of `model_1` as a valid data + source for subsequent use in ``Client.set_data_source()``. + + .. code-block:: bash + + 1) "model_1.ml_model" + + To delete the ML model in the ``Orchestrator``, we provide self ``Model`` `name` + to ``Client.set_data_source()`` then execute the function ``delete_model()``: + + .. code-block:: python + + # Set the Model source name + client.set_data_source("model_1") + # Delete the ML model + client.delete_model("ml_model") + + **Delete A ML Model Placed By An Alternate Application** + + SmartSim supports delete operations on prefixed ML models sent to the ``Orchestrator`` by separate ``Models``. + To do so, users must provide the ``Model`` `name` that stored the ML model to ``Client.set_data_source()``. + This will instruct the ``Client`` to prepend the ``Model`` `name` input to all key searches. + + In the example, a ``Model`` named `model_1` has placed a ML model in a standalone ``Orchestrator`` with prefixing enabled + on the ``Client``. The ``Orchestrator`` contents are: + + .. code-block:: bash + + 1) "model_1.ml_model" + + From within a separate ``Model`` named `model_2`, we perform a basic delete operation. + To instruct the ``Client`` to prepend a ``Model`` `name` to all key searches, use the + ``Client.set_data_source()`` function. Specify the ``Model`` name `model_1` + that placed the list in the ``Orchestrator``: + + .. code-block:: python + + client.set_data_source("model_1") + + To delete the ML model in the ``Orchestrator``, we provide the ML model `name`: + + .. code-block:: python + + client.delete_model("ml_model") + + .. group-tab:: Script + + **Delete Scripts From The Application They Were Loaded In** + + SmartSim supports delete operations on prefixed scripts sent to the ``Orchestrator`` from within + the same application that the script was placed. To achieve this, users must + provide the ``Model`` `name` that stored the script to ``Client.set_data_source()``. This action + instructs the ``Client`` to prepend the ``Model`` name to all key names. For SmartSim to + recognize the ``Model`` `name` as a data source, users must execute the + ``Model.register_incoming_entity()`` function on the ``Model`` and pass the self ``Model`` `name`. + + As an example, we placed a prefixed script on the ``Orchestrator`` within a ``Model`` named + `model_1`. The ``Orchestrator`` contents are: + + .. code-block:: bash + + 1) "model_1.script" + + .. note:: + In the driver script, after initializing the ``Model`` instance named `model_1`, + we execute ``model_1.register_incoming_entity(model_1)``. By passing the ``Model`` + instance to itself, we instruct SmartSim to recognize the name of `model_1` as a valid data + source for subsequent use in ``Client.set_data_source()``. + + To delete the script in the ``Orchestrator``, we provide the full list `name`: + + .. code-block:: python + + # Set the Model source name + client.set_data_source("model_1") + # Rename the script + client.delete_script("script") + + **Delete A Script Placed By An Alternate Application** + + SmartSim supports delete operations on prefixed scripts sent to the ``Orchestrator`` by separate ``Models``. + To do so, users must provide the ``Model`` name that stored the script to ``Client.set_data_source()``. + This will instruct the ``Client`` to prepend the ``Model`` name input to all key searches. + + In the example, a ``Model`` named `model_1` has placed a ML model in a standalone ``Orchestrator`` with prefixing enabled + on the ``Client``. The ``Orchestrator`` contents are: + + .. code-block:: bash + + 1) "model_1.script" + + From within a separate ``Model`` named `model_2`, we perform a basic delete operation. + To instruct the ``Client`` to prepend a ``Model`` name to all key searches, use the + ``Client.set_data_source()`` function. Specify the ``Model`` name `model_1` + that placed the list in the ``Orchestrator``: + + .. code-block:: python + + client.set_data_source("model_1") + + To delete the script in the ``Orchestrator``, we provide the script `name`: + + .. code-block:: python + + client.delete_model("script") \ No newline at end of file diff --git a/doc/requirements-doc.txt b/doc/requirements-doc.txt index 38d9c8052..e883a2805 100644 --- a/doc/requirements-doc.txt +++ b/doc/requirements-doc.txt @@ -6,9 +6,9 @@ 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 +torch==2.0.1 +tensorflow==2.13.1 ipython jinja2==3.1.2 protobuf -numpy \ No newline at end of file +numpy diff --git a/smartsim/_core/_cli/build.py b/smartsim/_core/_cli/build.py index 474d96c8a..5f1e7f051 100644 --- a/smartsim/_core/_cli/build.py +++ b/smartsim/_core/_cli/build.py @@ -60,14 +60,6 @@ def check_py_onnx_version(versions: Versioner) -> None: """Check Python environment for ONNX installation""" - if not versions.ONNX: - py_version = sys.version_info - 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 for ONNX 1.11 support" - ) - raise SetupError(msg) _check_packages_in_python_env( { "onnx": Version_(versions.ONNX), @@ -153,7 +145,7 @@ def build_redis_ai( backends_table = [ ["PyTorch", versions.TORCH, color_bool(use_torch)], ["TensorFlow", versions.TENSORFLOW, color_bool(use_tf)], - ["ONNX", versions.ONNX or "Unavailable", color_bool(use_onnx)], + ["ONNX", versions.ONNX, color_bool(use_onnx)], ] print(tabulate(backends_table, tablefmt="fancy_outline"), end="\n\n") print(f"Building for GPU support: {color_bool(device == 'gpu')}\n") @@ -226,9 +218,10 @@ def build_redis_ai( logger.info("ML Backends and RedisAI build complete!") -def check_py_torch_version(versions: Versioner, device: _TDeviceStr = "cpu") -> None: +def check_py_torch_version(versions: Versioner, device_in: _TDeviceStr = "cpu") -> None: """Check Python environment for TensorFlow installation""" + device = device_in.lower() if BuildEnv.is_macos(): if device == "gpu": raise BuildError("SmartSim does not support GPU on MacOS") @@ -260,10 +253,11 @@ def check_py_torch_version(versions: Versioner, device: _TDeviceStr = "cpu") -> "Torch version not found in python environment. " "Attempting to install via `pip`" ) + wheel_device = device if device == "cpu" else device_suffix.replace("+","") pip( "install", - "-f", - "https://download.pytorch.org/whl/torch_stable.html", + "--extra-index-url", + f"https://download.pytorch.org/whl/{wheel_device}", *(f"{package}=={version}" for package, version in torch_deps.items()), ) elif missing or conflicts: diff --git a/smartsim/_core/_install/buildenv.py b/smartsim/_core/_install/buildenv.py index eaa2c68bd..f8e2b5a37 100644 --- a/smartsim/_core/_install/buildenv.py +++ b/smartsim/_core/_install/buildenv.py @@ -193,23 +193,17 @@ class RedisAIVersion(Version_): defaults = { "1.2.7": { - "tensorflow": "2.8.0", - "onnx": "1.11.0", - "skl2onnx": "1.11.1", - "onnxmltools": "1.11.1", - "scikit-learn": "1.1.1", - "torch": "1.11.0", + "tensorflow": "2.13.1", + "onnx": "1.14.1", + "skl2onnx": "1.16.0", + "onnxmltools": "1.12.0", + "scikit-learn": "1.3.2", + "torch": "2.0.1", "torch_cpu_suffix": "+cpu", - "torch_cuda_suffix": "+cu113", - "torchvision": "0.12.0", + "torch_cuda_suffix": "+cu117", + "torchvision": "0.15.2", }, } - # Remove options with unsported wheels for python>=3.10 - if sys.version_info >= (3, 10): - 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") def __init__(self, vers: str) -> None: # pylint: disable=super-init-not-called min_rai_version = min(Version_(ver) for ver in self.defaults) @@ -304,33 +298,19 @@ class Versioner: # TensorFlow and ONNX only use the defaults, but these are not built into # the RedisAI package and therefore the user is free to pick other versions. TENSORFLOW = Version_(REDISAI.tensorflow) - try: - ONNX = Version_(REDISAI.onnx) - except AttributeError: - ONNX = None - - def as_dict(self, db_name: DbEngine = "REDIS") -> t.Dict[str, t.Any]: - packages = [ - "SMARTSIM", - "SMARTREDIS", - db_name, - "REDISAI", - "TORCH", - "TENSORFLOW", - ] - versions = [ - self.SMARTSIM, - self.SMARTREDIS, - self.REDIS, - self.REDISAI, - self.TORCH, - self.TENSORFLOW, - ] - if self.ONNX: - packages.append("ONNX") - versions.append(self.ONNX) - vers = {"Packages": packages, "Versions": versions} - return vers + ONNX = Version_(REDISAI.onnx) + + def as_dict(self, db_name: DbEngine = "REDIS") -> t.Dict[str, t.Tuple[str, ...]]: + pkg_map = { + "SMARTSIM": self.SMARTSIM, + "SMARTREDIS": self.SMARTREDIS, + db_name: self.REDIS, + "REDISAI": self.REDISAI, + "TORCH": self.TORCH, + "TENSORFLOW": self.TENSORFLOW, + "ONNX": self.ONNX, + } + return {"Packages": tuple(pkg_map), "Versions": tuple(pkg_map.values())} def ml_extras_required(self) -> t.Dict[str, t.List[str]]: """Optional ML/DL dependencies we suggest for the user. diff --git a/smartsim/_core/_install/builder.py b/smartsim/_core/_install/builder.py index f96a9bb5f..ef234228b 100644 --- a/smartsim/_core/_install/builder.py +++ b/smartsim/_core/_install/builder.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 concurrent.futures +import enum import os +import platform import re import shutil import stat import subprocess import sys +import tarfile +import tempfile import typing as t +import urllib.request +import zipfile +from abc import ABC, abstractmethod +from dataclasses import dataclass from pathlib import Path from shutil import which from subprocess import SubprocessError @@ -43,6 +52,10 @@ # - check cmake version and use system if possible to avoid conflicts TRedisAIBackendStr = t.Literal["tensorflow", "torch", "onnxruntime", "tflite"] +TDeviceStr = t.Literal["cpu", "gpu"] + +_T = t.TypeVar("_T") +_U = t.TypeVar("_U") def expand_exe_path(exe: str) -> str: @@ -69,6 +82,31 @@ class BuildError(Exception): pass +class Architecture(enum.Enum): + X64 = ("x86_64", "amd64") + + @classmethod + def from_str(cls, string: str, /) -> "Architecture": + string = string.lower() + for type_ in cls: + if string in type_.value: + return type_ + raise BuildError(f"Unrecognized or unsupported architecture: {string}") + + +class OperatingSystem(enum.Enum): + LINUX = ("linux", "linux2") + DARWIN = ("darwin",) + + @classmethod + def from_str(cls, string: str, /) -> "OperatingSystem": + string = string.lower() + for type_ in cls: + if string in type_.value: + return type_ + raise BuildError(f"Unrecognized or unsupported operating system: {string}") + + class Builder: """Base class for building third-party libraries""" @@ -99,12 +137,7 @@ def __init__( self.bin_path = dependency_path / "bin" self.lib_path = dependency_path / "lib" - - # Set wether build process will output to std output - self.out: t.Optional[int] = subprocess.DEVNULL self.verbose = verbose - if self.verbose: - self.out = None # make build directory "SmartSim/smartsim/_core/.third-party" if not self.build_dir.is_dir(): @@ -117,12 +150,18 @@ def __init__( self.jobs = jobs + @property + def out(self) -> t.Optional[int]: + return None if self.verbose else subprocess.DEVNULL + # implemented in base classes @property def is_built(self) -> bool: raise NotImplementedError - def build_from_git(self, git_url: str, branch: str, device: str = "cpu") -> None: + def build_from_git( + self, git_url: str, branch: str, device: TDeviceStr = "cpu" + ) -> None: raise NotImplementedError @staticmethod @@ -213,7 +252,9 @@ def is_built(self) -> bool: keydb_files = {"keydb-server", "keydb-cli"} return redis_files.issubset(bin_files) or keydb_files.issubset(bin_files) - def build_from_git(self, git_url: str, branch: str, device: str = "cpu") -> None: + def build_from_git( + self, git_url: str, branch: str, device: TDeviceStr = "cpu" + ) -> None: """Build Redis from git :param git_url: url from which to retrieve Redis :type git_url: str @@ -288,6 +329,32 @@ def build_from_git(self, git_url: str, branch: str, device: str = "cpu") -> None raise BuildError("Installation of redis-cli failed!") from e +class _RAIBuildDependency(ABC): + """An interface with a collection of magic methods so that + ``RedisAIBuilder`` can fetch and place its own dependencies + """ + + @property + @abstractmethod + def __rai_dependency_name__(self) -> str: ... + @abstractmethod + def __place_for_rai__(self, target: t.Union[str, "os.PathLike[str]"]) -> Path: ... + + +def _place_rai_dep_at( + target: t.Union[str, "os.PathLike[str]"], verbose: bool +) -> t.Callable[[_RAIBuildDependency], Path]: + def _place(dep: _RAIBuildDependency) -> Path: + if verbose: + print(f"Placing: '{dep.__rai_dependency_name__}'") + path = dep.__place_for_rai__(target) + if verbose: + print(f"Placed: '{dep.__rai_dependency_name__}' at '{path}'") + return path + + return _place + + class RedisAIBuilder(Builder): """Class to build RedisAI from Source Supported build method: @@ -317,6 +384,11 @@ def __init__( self.libtf_dir = libtf_dir self.torch_dir = torch_dir + # TODO: It might be worth making these constructor args so that users + # of this class can configure exactly _what_ they are building. + self._os = OperatingSystem.from_str(platform.system()) + self._architecture = Architecture.from_str(platform.machine()) + @property def rai_build_path(self) -> Path: return Path(self.build_dir, "RedisAI") @@ -351,6 +423,40 @@ def build_onnx(self) -> bool: def fetch_onnx(self) -> bool: return self.build_onnx + def get_deps_dir_path_for(self, device: TDeviceStr) -> Path: + def fail_to_format(reason: str) -> BuildError: # pragma: no cover + return BuildError(f"Failed to format RedisAI dependency path: {reason}") + + if self._os == OperatingSystem.DARWIN: + os_ = "macos" + elif self._os == OperatingSystem.LINUX: + os_ = "linux" + else: # pragma: no cover + raise fail_to_format(f"Unknown operating system: {self._os}") + if self._architecture == Architecture.X64: + arch = "x64" + else: # pragma: no cover + raise fail_to_format(f"Unknown architecture: {self._architecture}") + return self.rai_build_path / f"deps/{os_}-{arch}-{device}" + + def _get_deps_to_fetch_for( + self, device: TDeviceStr + ) -> t.Tuple[_RAIBuildDependency, ...]: + os_ = self._os + arch = self._architecture + # TODO: It would be nice if the backend version numbers were declared + # alongside the python package version numbers so that all of the + # dependency versions were declared in single location. + # Unfortunately importing into this module is non-trivial as it + # is used as script in the SmartSim `setup.py`. + fetchable_deps: t.Sequence[t.Tuple[bool, _RAIBuildDependency]] = ( + (True, _DLPackRepository("v0.5_RAI")), + (self.fetch_torch, _PTArchive(os_, device, "2.0.1")), + (self.fetch_tf, _TFArchive(os_, arch, device, "2.13.1")), + (self.fetch_onnx, _ORTArchive(os_, device, "1.16.3")), + ) + return tuple(dep for should_fetch, dep in fetchable_deps if should_fetch) + def symlink_libtf(self, device: str) -> None: """Add symbolic link to available libtensorflow in RedisAI deps. @@ -406,7 +512,9 @@ def symlink_libtf(self, device: str) -> None: if not dst_file.is_file(): os.symlink(src_file, dst_file) - def build_from_git(self, git_url: str, branch: str, device: str = "cpu") -> None: + def build_from_git( + self, git_url: str, branch: str, device: TDeviceStr = "cpu" + ) -> None: """Build RedisAI from git :param git_url: url from which to retrieve RedisAI @@ -432,56 +540,14 @@ def build_from_git(self, git_url: str, branch: str, device: str = "cpu") -> None "clone", "--recursive", git_url, + "--branch", + branch, + "--depth=1", + os.fspath(self.rai_build_path), ] - checkout_osx_fix: t.List[str] = [] - - # 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 branch == "v1.2.7": - # Clone RAI patch commit for OSX - clone_cmd += ["RedisAI"] - checkout_osx_fix = [ - "git", - "checkout", - "634916c722e718cc6ea3fad46e63f7d798f9adc2", - ] - else: - # Clone RAI release commit for versions > 1.2.7 - clone_cmd += [ - "--branch", - branch, - "--depth=1", - "RedisAI", - ] - self.run_command(clone_cmd, out=subprocess.DEVNULL, cwd=self.build_dir) - if checkout_osx_fix: - self.run_command( - checkout_osx_fix, out=subprocess.DEVNULL, cwd=self.rai_build_path - ) - - # get RedisAI dependencies - dep_cmd = self._rai_build_env_prefix( - with_pt=self.build_torch, - with_tf=self.build_tf, - with_ort=self.build_onnx, - extra_env={"VERBOSE": "1"}, - ) - - dep_cmd.extend( - [ - self.binary_path("bash"), - str(self.rai_build_path / "get_deps.sh"), - str(device), - ] - ) - - self.run_command( - dep_cmd, - out=subprocess.DEVNULL, # suppress this as it's not useful - cwd=self.rai_build_path, - ) + self._fetch_deps_for(device) if self.libtf_dir and device: self.symlink_libtf(device) @@ -541,6 +607,25 @@ def _rai_build_env_prefix( *(f"{key}={val}" for key, val in extra_env.items()), ] + def _fetch_deps_for(self, device: TDeviceStr) -> None: + if not self.rai_build_path.is_dir(): + raise BuildError("RedisAI build directory not found") + + deps_dir = self.get_deps_dir_path_for(device) + deps_dir.mkdir(parents=True, exist_ok=True) + if any(deps_dir.iterdir()): + raise BuildError("RAI build dependency directory is not empty") + to_fetch = self._get_deps_to_fetch_for(device) + placed_paths = _threaded_map( + _place_rai_dep_at(deps_dir, self.verbose), to_fetch + ) + unique_placed_paths = {os.fspath(path.resolve()) for path in placed_paths} + if len(unique_placed_paths) != len(to_fetch): + raise BuildError( + f"Expected to place {len(to_fetch)} dependencies, but only " + f"found {len(unique_placed_paths)}" + ) + def _install_backends(self, device: str) -> None: """Move backend libraries to smartsim/_core/lib/ :param device: cpu or cpu @@ -578,3 +663,231 @@ def _move_torch_libs(self) -> None: if sys.platform == "darwin": dylibs = pip_torch_path / ".dylibs" self.copy_dir(dylibs, ss_rai_torch_path / ".dylibs", set_exe=True) + + +def _threaded_map(fn: t.Callable[[_T], _U], items: t.Iterable[_T]) -> t.Sequence[_U]: + items = tuple(items) + if not items: # No items so no work to do + return () + num_workers = min(len(items), (os.cpu_count() or 4) * 5) + with concurrent.futures.ThreadPoolExecutor(num_workers) as pool: + return tuple(pool.map(fn, items)) + + +class _WebLocation(ABC): + @property + @abstractmethod + def url(self) -> str: ... + + +class _WebGitRepository(_WebLocation): + def clone( + self, + target: t.Union[str, "os.PathLike[str]"], + depth: t.Optional[int] = None, + branch: t.Optional[str] = None, + ) -> None: + depth_ = ("--depth", str(depth)) if depth is not None else () + branch_ = ("--branch", branch) if branch is not None else () + _git("clone", "-q", *depth_, *branch_, self.url, os.fspath(target)) + + +@t.final +@dataclass(frozen=True) +class _DLPackRepository(_WebGitRepository, _RAIBuildDependency): + version: str + + @property + def url(self) -> str: + return "https://github.com/RedisAI/dlpack.git" + + @property + def __rai_dependency_name__(self) -> str: + return f"dlpack@{self.url}" + + def __place_for_rai__(self, target: t.Union[str, "os.PathLike[str]"]) -> Path: + target = Path(target) / "dlpack" + self.clone(target, branch=self.version, depth=1) + if not target.is_dir(): + raise BuildError("Failed to place dlpack") + return target + + +class _WebArchive(_WebLocation): + @property + def name(self) -> str: + _, name = self.url.rsplit("/", 1) + return name + + def download(self, target: t.Union[str, "os.PathLike[str]"]) -> Path: + target = Path(target) + if target.is_dir(): + target = target / self.name + file, _ = urllib.request.urlretrieve(self.url, target) + return Path(file).resolve() + + +class _ExtractableWebArchive(_WebArchive, ABC): + @abstractmethod + def _extract_download( + self, download_path: Path, target: t.Union[str, "os.PathLike[str]"] + ) -> None: ... + + def extract(self, target: t.Union[str, "os.PathLike[str]"]) -> None: + with tempfile.TemporaryDirectory() as tmp_dir: + arch_path = self.download(tmp_dir) + self._extract_download(arch_path, target) + + +class _WebTGZ(_ExtractableWebArchive): + def _extract_download( + self, download_path: Path, target: t.Union[str, "os.PathLike[str]"] + ) -> None: + with tarfile.open(download_path, "r") as tgz_file: + tgz_file.extractall(target) + + +class _WebZip(_ExtractableWebArchive): + def _extract_download( + self, download_path: Path, target: t.Union[str, "os.PathLike[str]"] + ) -> None: + with zipfile.ZipFile(download_path, "r") as zip_file: + zip_file.extractall(target) + + +@t.final +@dataclass(frozen=True) +class _PTArchive(_WebZip, _RAIBuildDependency): + os_: OperatingSystem + device: TDeviceStr + version: str + + @property + def url(self) -> str: + if self.os_ == OperatingSystem.LINUX: + if self.device == "gpu": + pt_build = "cu117" + else: + pt_build = "cpu" + # pylint: disable-next=line-too-long + libtorch_arch = f"libtorch-cxx11-abi-shared-without-deps-{self.version}%2B{pt_build}.zip" + elif self.os_ == OperatingSystem.DARWIN: + if self.device == "gpu": + raise BuildError("RedisAI does not currently support GPU on Macos") + pt_build = "cpu" + libtorch_arch = f"libtorch-macos-{self.version}.zip" + else: + raise BuildError(f"Unexpected OS for the PT Archive: {self.os_}") + return f"https://download.pytorch.org/libtorch/{pt_build}/{libtorch_arch}" + + @property + def __rai_dependency_name__(self) -> str: + return f"libtorch@{self.url}" + + def __place_for_rai__(self, target: t.Union[str, "os.PathLike[str]"]) -> Path: + self.extract(target) + target = Path(target) / "libtorch" + if not target.is_dir(): + raise BuildError("Failed to place RAI dependency: `libtorch`") + return target + + +@t.final +@dataclass(frozen=True) +class _TFArchive(_WebTGZ, _RAIBuildDependency): + os_: OperatingSystem + architecture: Architecture + device: TDeviceStr + version: str + + @property + def url(self) -> str: + if self.architecture == Architecture.X64: + tf_arch = "x86_64" + else: + raise BuildError( + "Unexpected Architecture for TF Archive: {self.architecture}" + ) + + if self.os_ == OperatingSystem.LINUX: + tf_os = "linux" + tf_device = self.device + elif self.os_ == OperatingSystem.DARWIN: + tf_os = "darwin" + if self.device == "gpu": + raise BuildError("RedisAI does not currently support GPU on Macos") + tf_device = "cpu" + else: + raise BuildError("Unexpected OS for TF Archive: {self.os_}") + return ( + "https://storage.googleapis.com/tensorflow/libtensorflow/" + f"libtensorflow-{tf_device}-{tf_os}-{tf_arch}-{self.version}.tar.gz" + ) + + @property + def __rai_dependency_name__(self) -> str: + return f"libtensorflow@{self.url}" + + def __place_for_rai__(self, target: t.Union[str, "os.PathLike[str]"]) -> Path: + target = Path(target) / "libtensorflow" + target.mkdir() + self.extract(target) + return target + + +@t.final +@dataclass(frozen=True) +class _ORTArchive(_WebTGZ, _RAIBuildDependency): + os_: OperatingSystem + device: TDeviceStr + version: str + + @property + def url(self) -> str: + ort_url_base = ( + "https://github.com/microsoft/onnxruntime/releases/" + f"download/v{self.version}" + ) + if self.os_ == OperatingSystem.LINUX: + ort_os = "linux" + ort_arch = "x64" + ort_build = "-gpu" if self.device == "gpu" else "" + elif self.os_ == OperatingSystem.DARWIN: + ort_os = "osx" + ort_arch = "x86_64" + ort_build = "" + if self.device == "gpu": + raise BuildError("RedisAI does not currently support GPU on Macos") + else: + raise BuildError("Unexpected OS for TF Archive: {self.os_}") + ort_archive = f"onnxruntime-{ort_os}-{ort_arch}{ort_build}-{self.version}.tgz" + return f"{ort_url_base}/{ort_archive}" + + @property + def __rai_dependency_name__(self) -> str: + return f"onnxruntime@{self.url}" + + def __place_for_rai__(self, target: t.Union[str, "os.PathLike[str]"]) -> Path: + target = Path(target).resolve() / "onnxruntime" + self.extract(target) + try: + (extracted_dir,) = target.iterdir() + except ValueError: + raise BuildError( + "Unexpected number of files extracted from ORT archive" + ) from None + for file in extracted_dir.iterdir(): + file.rename(target / file.name) + extracted_dir.rmdir() + return target + + +def _git(*args: str) -> None: + git = Builder.binary_path("git") + cmd = (git,) + args + with subprocess.Popen(cmd) as proc: + proc.wait() + if proc.returncode != 0: + raise BuildError( + f"Command `{' '.join(cmd)}` failed with exit code {proc.returncode}" + ) diff --git a/tests/test_buildenv.py b/tests/install/test_buildenv.py similarity index 100% rename from tests/test_buildenv.py rename to tests/install/test_buildenv.py diff --git a/tests/install/test_builder.py b/tests/install/test_builder.py new file mode 100644 index 000000000..e6d030133 --- /dev/null +++ b/tests/install/test_builder.py @@ -0,0 +1,207 @@ +# 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 + +import functools +import pathlib +import platform +import threading +import time + +import smartsim._core._install.builder as build + +# The tests in this file belong to the group_a group +pytestmark = pytest.mark.group_a + + +for_each_device = pytest.mark.parametrize("device", ["cpu", "gpu"]) + +_toggle_build_optional_backend = lambda backend: pytest.mark.parametrize( + f"build_{backend}", + [ + pytest.param(switch, id=f"with{'' if switch else 'out'}-{backend}") + for switch in (True, False) + ], +) +toggle_build_tf = _toggle_build_optional_backend("tf") +toggle_build_pt = _toggle_build_optional_backend("pt") +toggle_build_ort = _toggle_build_optional_backend("ort") + + +@pytest.mark.parametrize( + "mock_os", [pytest.param(os_, id=f"os='{os_}'") for os_ in ("Windows", "Java", "")] +) +def test_rai_builder_raises_on_unsupported_op_sys(monkeypatch, mock_os): + monkeypatch.setattr(platform, "system", lambda: mock_os) + with pytest.raises(build.BuildError, match="operating system") as err_info: + build.RedisAIBuilder() + + +@pytest.mark.parametrize( + "mock_arch", + [ + pytest.param(arch_, id=f"arch='{arch_}'") + for arch_ in ("i386", "i686", "i86pc", "aarch64", "arm64", "armv7l", "") + ], +) +def test_rai_builder_raises_on_unsupported_architecture(monkeypatch, mock_arch): + monkeypatch.setattr(platform, "machine", lambda: mock_arch) + with pytest.raises(build.BuildError, match="architecture"): + build.RedisAIBuilder() + + +@pytest.fixture +def p_test_dir(test_dir): + yield pathlib.Path(test_dir).resolve() + + +@for_each_device +def test_rai_builder_raises_if_attempting_to_place_deps_when_build_dir_dne( + monkeypatch, p_test_dir, device +): + monkeypatch.setattr( + build.RedisAIBuilder, + "rai_build_path", + property(lambda self: p_test_dir / "path/to/dir/that/dne"), + ) + rai_builder = build.RedisAIBuilder() + with pytest.raises(build.BuildError, match=r"build directory not found"): + rai_builder._fetch_deps_for(device) + + +@for_each_device +def test_rai_builder_raises_if_attempting_to_place_deps_in_nonempty_dir( + monkeypatch, p_test_dir, device +): + (p_test_dir / "some_file.txt").touch() + monkeypatch.setattr( + build.RedisAIBuilder, "rai_build_path", property(lambda self: p_test_dir) + ) + monkeypatch.setattr( + build.RedisAIBuilder, "get_deps_dir_path_for", lambda *a, **kw: p_test_dir + ) + rai_builder = build.RedisAIBuilder() + + with pytest.raises(build.BuildError, match=r"is not empty"): + rai_builder._fetch_deps_for(device) + + +def _confirm_inst_presence(type_, should_be_present, seq): + expected_num_occurrences = 1 if should_be_present else 0 + occurrences = filter(lambda item: isinstance(item, type_), seq) + return expected_num_occurrences == len(tuple(occurrences)) + + +# Helper functions to check for the presence (or absence) of a +# ``_RAIBuildDependency`` dependency in a list of dependencies that need to be +# fetched by a ``RedisAIBuilder`` instance +dlpack_dep_presence = functools.partial( + _confirm_inst_presence, build._DLPackRepository, True +) +pt_dep_presence = functools.partial(_confirm_inst_presence, build._PTArchive) +tf_dep_presence = functools.partial(_confirm_inst_presence, build._TFArchive) +ort_dep_presence = functools.partial(_confirm_inst_presence, build._ORTArchive) + + +@for_each_device +@toggle_build_tf +@toggle_build_pt +@toggle_build_ort +def test_rai_builder_will_add_dep_if_backend_requested_wo_duplicates( + device, build_tf, build_pt, build_ort +): + rai_builder = build.RedisAIBuilder( + build_tf=build_tf, build_torch=build_pt, build_onnx=build_ort + ) + requested_backends = rai_builder._get_deps_to_fetch_for(device) + assert dlpack_dep_presence(requested_backends) + assert tf_dep_presence(build_tf, requested_backends) + assert pt_dep_presence(build_pt, requested_backends) + assert ort_dep_presence(build_ort, requested_backends) + + +@for_each_device +@toggle_build_tf +@toggle_build_pt +def test_rai_builder_will_not_add_dep_if_custom_dep_path_provided( + device, p_test_dir, build_tf, build_pt +): + mock_ml_lib = p_test_dir / "some/ml/lib" + mock_ml_lib.mkdir(parents=True) + rai_builder = build.RedisAIBuilder( + build_tf=build_tf, + build_torch=build_pt, + build_onnx=False, + libtf_dir=str(mock_ml_lib if build_tf else ""), + torch_dir=str(mock_ml_lib if build_pt else ""), + ) + requested_backends = rai_builder._get_deps_to_fetch_for(device) + assert dlpack_dep_presence(requested_backends) + assert tf_dep_presence(False, requested_backends) + assert pt_dep_presence(False, requested_backends) + assert ort_dep_presence(False, requested_backends) + assert len(requested_backends) == 1 + + +def test_rai_builder_raises_if_it_fetches_an_unexpected_number_of_ml_deps( + monkeypatch, p_test_dir +): + monkeypatch.setattr( + build.RedisAIBuilder, "rai_build_path", property(lambda self: p_test_dir) + ) + monkeypatch.setattr( + build, + "_place_rai_dep_at", + lambda target, verbose: lambda dep: target + / "whoops_all_ml_deps_extract_to_a_dir_with_this_name", + ) + rai_builder = build.RedisAIBuilder(build_tf=True, build_torch=True, build_onnx=True) + with pytest.raises( + build.BuildError, + match=r"Expected to place \d+ dependencies, but only found \d+", + ): + rai_builder._fetch_deps_for("cpu") + + +def test_threaded_map(): + def _some_io_op(x): + return x * x + + assert (0, 1, 4, 9, 16) == tuple(build._threaded_map(_some_io_op, range(5))) + + +def test_threaded_map_returns_early_if_nothing_to_map(): + sleep_duration = 60 + + def _some_long_io_op(_): + time.sleep(sleep_duration) + + start = time.time() + build._threaded_map(_some_long_io_op, []) + end = time.time() + assert end - start < sleep_duration diff --git a/tutorials/ml_inference/Inference-in-SmartSim.ipynb b/tutorials/ml_inference/Inference-in-SmartSim.ipynb index 384c46d69..711ae999c 100644 --- a/tutorials/ml_inference/Inference-in-SmartSim.ipynb +++ b/tutorials/ml_inference/Inference-in-SmartSim.ipynb @@ -74,7 +74,7 @@ "\n", "Build SmartSim dependencies (Redis, RedisAI, ML runtimes)\n", "\n", - "optional arguments:\n", + "options:\n", " -h, --help show this help message and exit\n", " -v Enable verbose build process\n", " --device {cpu,gpu} Device to build ML runtimes for\n", @@ -129,16 +129,16 @@ "\n", "ML Backends Requested\n", "╒════════════╤════════╤══════╕\n", - "│ PyTorch │ 1.11.0 │ \u001b[32mTrue\u001b[0m │\n", - "│ TensorFlow │ 2.8.0 │ \u001b[32mTrue\u001b[0m │\n", - "│ ONNX │ 1.11.0 │ \u001b[32mTrue\u001b[0m │\n", + "│ PyTorch │ 2.0.1 │ \u001b[32mTrue\u001b[0m │\n", + "│ TensorFlow │ 2.13.1 │ \u001b[32mTrue\u001b[0m │\n", + "│ ONNX │ 1.14.1 │ \u001b[32mTrue\u001b[0m │\n", "╘════════════╧════════╧══════╛\n", "\n", "Building for GPU support: \u001b[31mFalse\u001b[0m\n", "\n", "\u001b[34m[SmartSim]\u001b[0m \u001b[1;30mINFO\u001b[0m Building RedisAI version 1.2.7 from https://github.com/RedisAI/RedisAI.git/\n", "\u001b[34m[SmartSim]\u001b[0m \u001b[1;30mINFO\u001b[0m ML Backends and RedisAI build complete!\n", - "\u001b[34m[SmartSim]\u001b[0m \u001b[1;30mINFO\u001b[0m Tensorflow, Torch, Onnxruntime backend(s) built\n", + "\u001b[34m[SmartSim]\u001b[0m \u001b[1;30mINFO\u001b[0m Tensorflow, Onnxruntime, Torch backend(s) built\n", "\u001b[34m[SmartSim]\u001b[0m \u001b[1;30mINFO\u001b[0m SmartSim build complete!\n" ] } @@ -351,48 +351,46 @@ "name": "stdout", "output_type": "stream", "text": [ - "SmartRedis Library@23-56-41:WARNING: Environment variable SR_LOG_FILE is not set. Defaulting to stdout\n", - "SmartRedis Library@23-56-41:WARNING: Environment variable SR_LOG_LEVEL is not set. Defaulting to INFO\n", - "Prediction: [[-2.3274555 -2.3253717 -2.354757 -2.3729622 -2.3431003 -2.1907542\n", - " -2.3514638 -2.1824958 -2.3210742 -2.2772176]\n", - " [-2.319342 -2.3146112 -2.370425 -2.372699 -2.3437245 -2.1988375\n", - " -2.354674 -2.1797025 -2.3205185 -2.2724082]\n", - " [-2.316474 -2.3222082 -2.354598 -2.3659394 -2.3442194 -2.203955\n", - " -2.3561926 -2.1938426 -2.3158035 -2.2702417]\n", - " [-2.3319743 -2.311106 -2.356003 -2.3770962 -2.333499 -2.1953351\n", - " -2.3548756 -2.195049 -2.310809 -2.2787712]\n", - " [-2.3205962 -2.3178282 -2.3519592 -2.3816493 -2.3516834 -2.1981795\n", - " -2.3636622 -2.1777525 -2.3139138 -2.2705152]\n", - " [-2.3096914 -2.3222034 -2.3647196 -2.3790689 -2.3540542 -2.206103\n", - " -2.350227 -2.1878397 -2.3078933 -2.2638521]\n", - " [-2.3328648 -2.3219166 -2.3527567 -2.3824098 -2.3419397 -2.1949291\n", - " -2.3534136 -2.1831408 -2.31838 -2.2653728]\n", - " [-2.3125417 -2.324307 -2.3541815 -2.379772 -2.348383 -2.2018006\n", - " -2.3614779 -2.1773078 -2.322288 -2.2653532]\n", - " [-2.3261974 -2.3169107 -2.3658333 -2.372918 -2.3417373 -2.1894612\n", - " -2.3535395 -2.2018242 -2.308719 -2.268019 ]\n", - " [-2.316616 -2.3056076 -2.355318 -2.3717446 -2.346278 -2.1928883\n", - " -2.3632033 -2.2028553 -2.3090112 -2.2805274]\n", - " [-2.3209507 -2.3127859 -2.358682 -2.3774037 -2.3558414 -2.2000623\n", - " -2.3439143 -2.1920927 -2.3196788 -2.2638488]\n", - " [-2.3159695 -2.3109243 -2.356306 -2.374135 -2.3412004 -2.1999855\n", - " -2.3728766 -2.1851294 -2.3103416 -2.2791054]\n", - " [-2.320004 -2.3205712 -2.3569424 -2.3752837 -2.3463457 -2.1887283\n", - " -2.3645942 -2.1946917 -2.3067377 -2.272361 ]\n", - " [-2.310819 -2.3274822 -2.356091 -2.3715394 -2.3474889 -2.200722\n", - " -2.3434677 -2.1957805 -2.3201551 -2.2701602]\n", - " [-2.3143158 -2.31956 -2.358585 -2.362682 -2.3464782 -2.196579\n", - " -2.3578608 -2.2015376 -2.3066673 -2.2789493]\n", - " [-2.318907 -2.3225117 -2.3634868 -2.3806338 -2.344084 -2.1920872\n", - " -2.3534818 -2.1955805 -2.3039575 -2.2711294]\n", - " [-2.3084583 -2.3254113 -2.3642344 -2.3710778 -2.3496058 -2.192245\n", - " -2.3604536 -2.1796546 -2.310007 -2.286219 ]\n", - " [-2.3140576 -2.3124697 -2.359347 -2.379842 -2.3481016 -2.1948602\n", - " -2.3681424 -2.1851056 -2.3161757 -2.2693238]\n", - " [-2.3162746 -2.3137376 -2.3598473 -2.3751001 -2.3536685 -2.1899457\n", - " -2.3560162 -2.1918488 -2.3077402 -2.2818694]\n", - " [-2.3138344 -2.3119657 -2.3552136 -2.3767023 -2.3556495 -2.187487\n", - " -2.3484402 -2.1922355 -2.3236399 -2.2809098]]\n" + "Prediction: [[-2.1860428 -2.3318565 -2.2773128 -2.2742267 -2.2679536 -2.304159\n", + " -2.423439 -2.3406057 -2.2474668 -2.3950338]\n", + " [-2.1803837 -2.3286302 -2.2805855 -2.2874444 -2.261593 -2.3145547\n", + " -2.4357762 -2.3169715 -2.2618299 -2.3798223]\n", + " [-2.1833746 -2.3249795 -2.28497 -2.2851245 -2.2555952 -2.308204\n", + " -2.4274755 -2.3441646 -2.2553194 -2.3779805]\n", + " [-2.1843016 -2.3395848 -2.2619352 -2.294549 -2.2571433 -2.312943\n", + " -2.4161577 -2.338785 -2.2538524 -2.3881512]\n", + " [-2.1936755 -2.3315516 -2.2739122 -2.2832148 -2.2666094 -2.3038912\n", + " -2.4211216 -2.3300066 -2.2564852 -2.3846986]\n", + " [-2.1709712 -2.3271346 -2.280365 -2.286064 -2.2617233 -2.3227994\n", + " -2.4253702 -2.3313646 -2.2593162 -2.383301 ]\n", + " [-2.1948013 -2.3318067 -2.2713811 -2.2844 -2.2526758 -2.3178148\n", + " -2.4255004 -2.3233378 -2.2388031 -2.4088087]\n", + " [-2.17515 -2.3240736 -2.2818787 -2.2857373 -2.259629 -2.3184\n", + " -2.425821 -2.3519678 -2.2413275 -2.385761 ]\n", + " [-2.187554 -2.3335872 -2.2767708 -2.2818003 -2.2654893 -2.3097534\n", + " -2.4182632 -2.3376188 -2.2509694 -2.384327 ]\n", + " [-2.1793714 -2.340681 -2.271785 -2.287751 -2.2620957 -2.3163543\n", + " -2.4111845 -2.3468175 -2.2472064 -2.3842056]\n", + " [-2.1906679 -2.3483853 -2.2580595 -2.2923894 -2.25718 -2.2951608\n", + " -2.431815 -2.3487022 -2.2326546 -2.3963163]\n", + " [-2.1882055 -2.3293467 -2.2767649 -2.279892 -2.2527165 -2.3220086\n", + " -2.4226239 -2.3364902 -2.2455037 -2.394776 ]\n", + " [-2.1756573 -2.3318045 -2.2690601 -2.2737868 -2.264148 -2.3212118\n", + " -2.4243867 -2.3421402 -2.2562728 -2.390894 ]\n", + " [-2.1824148 -2.3317673 -2.2749603 -2.291667 -2.2524009 -2.3026595\n", + " -2.42986 -2.3290846 -2.265264 -2.387787 ]\n", + " [-2.1871543 -2.3408008 -2.2773213 -2.283908 -2.249834 -2.3159058\n", + " -2.4251873 -2.339211 -2.245001 -2.3839695]\n", + " [-2.1855574 -2.3216138 -2.2722392 -2.2826352 -2.2573392 -2.308948\n", + " -2.4348576 -2.3421624 -2.2397952 -2.4060655]\n", + " [-2.1876159 -2.330091 -2.2779942 -2.2849102 -2.2582757 -2.3122754\n", + " -2.4250498 -2.333003 -2.250753 -2.3871331]\n", + " [-2.182653 -2.3381891 -2.2795184 -2.287199 -2.2628696 -2.303869\n", + " -2.413879 -2.3404965 -2.26254 -2.3739154]\n", + " [-2.1733668 -2.3377435 -2.2724369 -2.28559 -2.2537165 -2.3127556\n", + " -2.4249415 -2.3484716 -2.2515364 -2.3897333]\n", + " [-2.1839535 -2.336417 -2.2839231 -2.285238 -2.2608624 -2.3198016\n", + " -2.424396 -2.3165755 -2.2433887 -2.3935702]]\n" ] } ], @@ -420,8 +418,8 @@ "source": [ "As we gave the CNN random noise, the predictions reflect that.\n", "\n", - "If running on CPU, be sure to change the argument in the ``set_model`` call\n", - "above to ``CPU``." + "If running on GPU, be sure to change the argument in the ``set_model`` call\n", + "above to ``device=\"GPU\"``." ] }, { @@ -468,46 +466,46 @@ "name": "stdout", "output_type": "stream", "text": [ - "U: [[[-0.550159 0.8065786 ]\n", - " [-0.52288723 -0.5346357 ]\n", - " [-0.6510868 -0.2521817 ]]\n", + "U: [[[-0.31189808 0.86989427]\n", + " [-0.48122275 -0.49140105]\n", + " [-0.81923395 -0.0425336 ]]\n", "\n", - " [[-0.17983183 -0.20003092]\n", - " [-0.5534476 -0.7888692 ]\n", - " [-0.81323797 0.58109635]]\n", + " [[-0.5889101 -0.29554686]\n", + " [-0.43949458 -0.66398275]\n", + " [-0.6782547 0.68686163]]\n", "\n", - " [[-0.20800859 0.42269117]\n", - " [-0.65485084 -0.7300564 ]\n", - " [-0.7265692 0.53698224]]\n", + " [[-0.61623317 0.05853765]\n", + " [-0.6667615 -0.5695148 ]\n", + " [-0.4191489 0.81989413]]\n", "\n", - " [[-0.336111 0.77894354]\n", - " [-0.31149226 0.43854192]\n", - " [-0.8888205 -0.44825 ]]\n", + " [[-0.5424681 0.8400398 ]\n", + " [-0.31990844 -0.2152339 ]\n", + " [-0.77678 -0.49800384]]\n", "\n", - " [[-0.6365824 0.7635661 ]\n", - " [-0.2663487 -0.08588188]\n", - " [-0.723755 -0.639993 ]]]\n", + " [[-0.43667376 0.8088193 ]\n", + " [-0.70812154 -0.57906115]\n", + " [-0.5548693 0.10246649]]]\n", "\n", - ", S: [[137.34267 54.616768]\n", - " [142.89323 35.937744]\n", - " [ 90.98083 48.821 ]\n", - " [ 86.74378 31.835794]\n", - " [146.14839 36.327038]]\n", + ", S: [[137.10924 25.710997]\n", + " [131.49983 37.79937 ]\n", + " [178.72423 24.792084]\n", + " [125.13014 49.733784]\n", + " [137.48834 53.57199 ]]\n", "\n", - ", V: [[[-0.48165366 0.8763617 ]\n", - " [-0.8763617 -0.48165366]]\n", + ", V: [[[-0.8333395 0.5527615 ]\n", + " [-0.5527615 -0.8333395 ]]\n", "\n", - " [[-0.47905296 0.8777859 ]\n", - " [-0.8777859 -0.47905296]]\n", + " [[-0.5085228 -0.8610485 ]\n", + " [-0.8610485 0.5085228 ]]\n", "\n", - " [[-0.737007 -0.67588514]\n", - " [-0.67588514 0.737007 ]]\n", + " [[-0.8650402 0.5017025 ]\n", + " [-0.5017025 -0.8650402 ]]\n", "\n", - " [[-0.28137407 0.9595981 ]\n", - " [-0.9595981 -0.28137407]]\n", + " [[-0.56953645 0.8219661 ]\n", + " [-0.8219661 -0.56953645]]\n", "\n", - " [[-0.5767642 -0.8169106 ]\n", - " [-0.8169106 0.5767642 ]]]\n", + " [[-0.6115895 0.79117525]\n", + " [-0.79117525 -0.6115895 ]]]\n", "\n" ] } @@ -594,8 +592,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "[[0.03525018 0.04472604 0.02831913 0.1114466 0.25944078 0.11165252\n", - " 0.2983908 0.04830809 0.02390536 0.03856055]]\n" + "[[0.05032112 0.06484107 0.03512685 0.14747524 0.14440396 0.02395445\n", + " 0.03395916 0.06222691 0.26738793 0.1703033 ]]\n" ] } ], @@ -657,8 +655,6 @@ "\n", "And PyTorch has its own converter.\n", "\n", - "Currently the ONNX backend only works on Linux, but MacOS support will be added in the future.\n", - "\n", "Below are some examples of a few models in [Scikit-learn](https://scikit-learn.org)\n", "that are converted into ONNX format for use with SmartSim. To use ONNX in SmartSim, specify\n", "`ONNX` as the argument for *backend* in the call to `client.set_model` or\n", @@ -801,15 +797,7 @@ "metadata": { "tags": [] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "23:56:50 C02G13RYMD6N SmartSim[33744] INFO Stopping model orchestrator_0 with job name orchestrator_0-CVIG02IVGHO0\n" - ] - } - ], + "outputs": [], "source": [ "exp.stop(db)" ] @@ -830,12 +818,12 @@ " Name Entity-Type JobID RunID Time Status Returncode \n", "\n", "\n", - "0 orchestrator_0DBNode 35628 0 29.7008Cancelled-9 \n", + "0 orchestrator_0DBNode 31857 0 32.7161Cancelled0 \n", "\n", "" ], "text/plain": [ - "'\\n\\n\\n\\n\\n\\n\\n
Name Entity-Type JobID RunID Time Status Returncode
0 orchestrator_0DBNode 35628 0 29.7008Cancelled-9
'" + "'\\n\\n\\n\\n\\n\\n\\n
Name Entity-Type JobID RunID Time Status Returncode
0 orchestrator_0DBNode 31857 0 32.7161Cancelled0
'" ] }, "execution_count": 19, @@ -844,7 +832,7 @@ } ], "source": [ - "exp.summary(format=\"html\")" + "exp.summary(style=\"html\")" ] }, { @@ -901,24 +889,24 @@ "name": "stdout", "output_type": "stream", "text": [ - "23:56:50 C02G13RYMD6N SmartSim[33744] INFO \n", + "21:18:06 C02G13RYMD6N SmartSim[30945] INFO \n", "\n", "=== Launch Summary ===\n", "Experiment: Inference-Tutorial\n", - "Experiment Path: /Users/mrdro/repos/ssimdev/ss/tutorials/ml_inference/Inference-Tutorial\n", + "Experiment Path: /Users/smartsim/smartsim/tutorials/ml_inference/Inference-Tutorial\n", "Launcher: local\n", "Models: 1\n", "Database Status: inactive\n", "\n", "=== Models ===\n", "colocated_model\n", - "Executable: /Users/mrdro/miniconda3/envs/smartsim/bin/python\n", + "Executable: /Users/smartsim/venv/bin/python\n", "Executable Arguments: ./colo-db-torch-example.py\n", "Co-located Database: True\n", "\n", "\n", "\n", - "23:56:52 C02G13RYMD6N SmartSim[33744] INFO colocated_model(35666): Completed\n" + "21:18:09 C02G13RYMD6N SmartSim[30945] INFO colocated_model(31865): Completed\n" ] } ], @@ -942,13 +930,13 @@ " Name Entity-Type JobID RunID Time Status Returncode \n", "\n", "\n", - "0 orchestrator_0 DBNode 35628 0 29.7008Cancelled-9 \n", - "1 colocated_modelModel 35666 0 2.1590 Completed0 \n", + "0 orchestrator_0 DBNode 31857 0 32.7161Cancelled0 \n", + "1 colocated_modelModel 31865 0 3.5862 Completed0 \n", "\n", "" ], "text/plain": [ - "'\\n\\n\\n\\n\\n\\n\\n\\n
Name Entity-Type JobID RunID Time Status Returncode
0 orchestrator_0 DBNode 35628 0 29.7008Cancelled-9
1 colocated_modelModel 35666 0 2.1590 Completed0
'" + "'\\n\\n\\n\\n\\n\\n\\n\\n
Name Entity-Type JobID RunID Time Status Returncode
0 orchestrator_0 DBNode 31857 0 32.7161Cancelled0
1 colocated_modelModel 31865 0 3.5862 Completed0
'" ] }, "execution_count": 22, @@ -957,7 +945,7 @@ } ], "source": [ - "exp.summary(format=\"html\")" + "exp.summary(style=\"html\")" ] } ], @@ -977,7 +965,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.17" + "version": "3.10.13" } }, "nbformat": 4, diff --git a/tutorials/online_analysis/lattice/online_analysis.ipynb b/tutorials/online_analysis/lattice/online_analysis.ipynb index 48ddf6032..3389b1190 100644 --- a/tutorials/online_analysis/lattice/online_analysis.ipynb +++ b/tutorials/online_analysis/lattice/online_analysis.ipynb @@ -345,7 +345,7 @@ "source": [ "## 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", + "We can upload [TorchScript functions](https://pytorch.org/docs/2.0/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",