1) Writing a basic application definition
This tutorial will provide an introduction to writing an application definition
in Ramble. In this tutorial, you will create and test an application definition
file to run the hostname linux utility as your application.
It is a good idea to have a basic working understanding of how to create and use Ramble workspaces before starting this tutorial. You should at least be familiar with the content of the Hello World Tutorial.
This tutorial is intended to be a practical, hands-on guide to creating a simple application definition. For a more comprehensive reference on all available directives and advanced features, please see the Application Definition Developers Guide.
Installation
To install Ramble, see the Getting Started guide.
NOTE: This tutorial does not require a package manager to be installed or configured.
Ramble Repositories
Before writing our definitions, we will create a repository to house the application definition. Repositories in Ramble can house any object type, and are not limited to only those in this tutorial.
The ramble repo command is used to manage object repositories in Ramble. To
create a new repository, execute the following:
$ ramble repo create tutorial-repo
This will create a new directory named tutorial-repo in your current
directory. Inside, directories will exist for each of the object types. These
will include things like applications and package_managers. Resulting
object paths in this repo would look like:
tutorial-repo/applications/hostname/application.py
You can also create a repository without object subdirectories using:
$ ramble repo create tutorial-repo -d ""
In this case, the resulting structure looks like:
tutorial-repo/hostname/application.py
This latter layout allows objects with the same name but different types to coexist in the same directory. For example, if there was a package manager named hostname, it would exist in the following path:
tutorial-repo/hostname/package_manager.py
The remaining commands in this tutorial will assume your repository layout matches the first example, but you can feel free to use either layout. Just map any paths to the correct layout.
The actual object files within the repository are named based on the object they represent. Below is a mapping of some object types to file names:
Application -
application.pyBase Application -
base_application.pyModifier -
modifier.pyPackage Manager -
package_manager.pyWorkflow Manager -
workflow_manager.pySystem -
system.pyPlatform -
platform.py
As listed with the application.py file, each object also has a
corresponding base version, that is mostly use to help inheritance into
several concrete objects.
Once your repository is created, you can register it with Ramble by issuing the following command:
$ ramble repo add tutorial-repo
NOTE: Ramble comes with a default builtin repository. Adding new
repositories gives them a higher precedence to other existing repositories.
Ramble uses this precedence ordering to decide which object definition is used
when multiple exist with the same name. Each repository has a namespace, and
these namespaces can be used to refer to specific instances of each object
definition.
Hostname Application Definition
For the remainder of this tutorial, we will be writing and testing the contents of the hostname application definition.
Create Application Definition
To begin with, we will create an empty application definition file. We will populate this file throughout the remainder of this tutorial. To create it, issue the following commands:
$ mkdir -p tutorial-repo/applications/hostname
$ touch tutorial-repo/applications/hostname/application.py
For the remainder of this tutorial, ramble edit hostname will open this
file with the editor specified with your EDITOR environment variable.
Application Class
Ramble provides a module (e.g. appkit) which imports a large portion of the
features to write application definitions. Each application definition should
import this using:
from ramble.appkit import *
Every application definition in Ramble contains a python class defines the
characteristics of the application. The name of this class matches the
directory name for the application, but converted to CamelCase. Since our
application name (hostname) does not have any hyphens or underscores in the
name, the python class name will be Hostname.
Ramble also provides a base class, ExecutableApplication which handles
applying the application language to the object. Every application definition
should inherit from either this, or something else that inherits from this
class.
Object definitions should have a name attribute that matches the directory name
as well. In this case: name = 'hostname'.
As a result, our class definition file should contain the following to start.
from ramble.appkit import *
class Hostname(ExecutableApplication):
name = 'hostname'
At this stage, our application should show up in the output of ramble list
and ramble info hostname should show limited information about this
application.
Definition Experiment Constructs
To begin with, we will create some basic constructs within this application definition that will help us test the creation of experiments.
Application Executables
The lowest level construct in an application definition is an executable.
Executables relate to arbitrary commands that you would normally execute when
running your specific application. Since we are writing an application
definition for hostname, our command simply looks like hostname. The
language feature executable can be used to define how Ramble should use
these. The documentation for the executable directive can be found at
ramble.language.application_language.executable().
For the purposes of this tutorial, we will begin by assuming we will not
execute hostname under an mpi runtime. The starting executable
definition will be:
executable(
"local-execute",
"hostname",
)
This defines a new executable in the hostname application definition named
local-execute and the template for the executable is simply the
hostname command. The remaining arguments are left as the default which
will disable MPI on this executable.
Application Workloads
Workloads are the construct that users refer to within workspaces to create
experiments. Workloads are the pairing of one or more executables with zero or
more input files. In the case of hostname we have no input files that are
required to run this application. The documentation for the workload directive
can be seen at: ramble.language.application_language.workload().
In our case, we will create a new workload that simply uses the
local-execute executable. This directive should look like:
workload("local", executables=["local-execute"])
At this stage, our application definition should look like the following:
from ramble.appkit import *
class Hostname(ExecutableApplication):
name = "hostname"
executable(
"local-execute",
"hostname"
)
workload("local", executables=["local-execute"])
With this application definition, we are now at a point where experiments can be constructed to test this definition file.
Testing Application Definitions
To exercise the application definition, we need to construct a workspace. To do this, execute the following:
$ ramble workspace create -d tutorial-workspace
NOTE: If you have an active workspace (e.g., if you used ramble workspace
create -a in a previous session or this one), you must first deactivate it
with ramble workspace deactivate or unset the RAMBLE_WORKSPACE
environment variable to avoid conflicts when creating a new workspace. Also,
creating a workspace without the -a (activate) flag means you will need
to use the -D <workspace_path> flag with subsequent ramble commands to
specify which workspace to operate on.
The following command can be used to add an experiment with the workload we defined earlier:
$ ramble workspace manage experiments hostname -v "n_nodes=1" -v "n_ranks=1" --overwrite
This will add a single experiment, named generated to the workspace that
will use the hostname application and the local workload (since this is the
only defined workload at the moment).
The experiments can then be set up using:
$ ramble workspace setup
The contents of the experiment’s execute_experiment script can be examined
to ensure it looks correct. It should be inside the workspace in the
experiments/hostname/local/generated/execute_experiment path. The contents
should look similar to the following (with some expected path differences):
#!/bin/bash
# This is a template execution script for
# running the execute pipeline.
#
# Variables surrounded by curly braces will be expanded
# when generating a specific execution script.
# Some example variables are:
# - experiment_run_dir (Will be replaced with the experiment directory)
# - command (Will be replaced with the command to run the experiment)
# - log_dir (Will be replaced with the logs directory)
# - experiment_name (Will be replaced with the name of the experiment)
# - workload_run_dir (Will be replaced with the directory of the workload
# - application_name (Will be replaced with the name of the application)
# - n_nodes (Will be replaced with the required number of nodes)
# Any experiment parameters will be available as variables as well.
# ****************************************************
# * No workflow is used with this experiment
# * Execution command: /tmp/tutorial-workspace/experiments/hostname/local/generated/execute_experiment
# * If this file is not the same as the above path, it is unlikely that this script
# * is used when `ramble on` executes experiments.
# ****************************************************
cd "/tmp/tutorial-workspace/experiments/hostname/local/generated"
rm -f "/tmp/tutorial-workspace/experiments/hostname/local/generated/generated.out"
touch "/tmp/tutorial-workspace/experiments/hostname/local/generated/generated.out"
export OMP_NUM_THREADS="1";
hostname >> "/tmp/tutorial-workspace/experiments/hostname/local/generated/generated.out" 2>&1
The last line of this file shows the hostname command will be run, and the output will be redirected to the experiment’s log file.
At this stage, this experiment can be executed (if you have access to the
hostname binary) to ensure it executes properly. This can be accomplished using:
$ ramble on
After executing, the output of the hostname command should exist in the
generated.out file inside the experiment’s directory.
This workflow can be used in the future to continue testing our application definition.
Workload Variables
Workload variables are a mechanism that application definition developers can use to expose aspects of an application or workload that users might want to control. These can be anything from input flags / arguments, to parameter definitions.
We will use workload variables to allow users to control the execution flags on
the hostname binary. While the default might be to only use the default
behavior of hostname, adding this functionality in allows the application
definition to be more flexible for users in the future.
The ramble.language.application_language.workload_variable() directive
is used to create a variable that users can easily know about. We will now
create a variable named input_arguments using the following directive:
workload_variable(
"input_arguments",
default="",
description="Input arguments for hostname",
workloads=["*"],
)
In this example, we set the default value to be "" which will retain the
default hostname behavior, we can write a description to provide
information to users about what the purpose of this variable is, and we can
control which workloads this variable are associated with. In this example, we
use ["*"] to glob all workloads and make it easier for this to be used on
all workloads. However, selecting specific workloads can allow developers to
change the default value of a variable based on the workload selected.
Now that we have a variable, we need to update the executable definition to
make sure it is used. The new local-execute definition should look like the
following:
executable(
"local-execute",
"hostname {input_arguments}"
)
This definition allows the local-execute executable to expand the value of
the input_arguments variable and append it to the hostname executable.
If a user had the following in their workspace config:
variables:
input_arguments: "-i"
The resulting rendered execute_experiment script will contain the -i
flag, and the output should be an IP address instead of a hostname.
At this point, the basic test can be used to see
how the input_arguments applies to experiments.
NOTE: When using the workload or workloads arguments on the
workload_variable directive, the directive needs to show up after the
workloads it is attached to within the python class. Usage of
workload_group s can mitigate this restriction.
Parallel Executables
Some applications need to be executed under some parallel runtime, such as MPI.
Within application definitions, developers can convey this to Ramble by adding
the use_mpi=True argument when defining new executables.
When this argument is set to True, Ramble will prepend the mpi_command
variable definition to the command line within the resulting execution script.
Users can control the value of the mpi_command from their workspace, or the
definition can come from other object definitions (such as
workflow_managers), however this is the mechanism for executables to say
they should be executed in parallel.
To see how this behaves, we will create a new workload that will represent the
parallel execution of hostname. In general, hostname only needs to be
executed once per node in the job. As a result, we will override the
n_ranks variable definition to match the n_nodes value.
executable(
"parallel-execute",
"hostname {input_arguments}",
use_mpi=True,
variables={"n_ranks": "{n_nodes}"},
)
workload("parallel", executables=["parallel-execute"])
To test this, we can follow the steps in the basic
test from earlier, which will now create experiments
for each of the two workloads. NOTE: If you had a value for
input_arguments running the commands as-is could remove these from your
workspace.
After setting up the workspace again, the parallel workload’s generated
execute_experiment script should contain:
mpirun -n 1 hostname >> "/tmp/tutorial-workspace/experiments/hostname/parallel/generated/generated.out" 2>&1
The default mpi_command from the user-managed workflow manager happens
to be mpirun -n {n_ranks}, which is prepended to our hostname
executable in this line.
NOTE: Execution of these experiments will fail if you do not have
mpirun on the system you’re running the experiments on. To execute only the
experiments with the local workload, you can use:
$ ramble on --where '"{workload_name}" == "local"'
Analysis of experiments
Up until this point, we have focused on constructing the execution of experiment. However, Ramble also handles analysis of the experiments. To do this, application definitions define figures of merit. A figure of merit is an arbitrary metric that Ramble should extract and track for each experiment generated from this application definition. Additionally, success criteria can be defined to help users know whether their experiment behaved the way it was expected to or not.
Figure of merit
To begin with, we will add a figure of merit using the
ramble.language.shared_language.figure_of_merit()
directive. Figures of merit are extracted using a regular expression match on
some file in the experiment directory. We will use the following definition to
track whatever the output from the experiment is as the possible hostname:
figure_of_merit(
"possible hostname",
fom_regex=r"(?P<hostname>\S+)",
group_name="hostname",
units="",
)
In this directive, the name possible_hostname will show up in the resulting
results file after analysis of a workspace. The fom_regex argument controls
what regular expression is used to extract this figure of merit. The
group_name argument controls which regular expression group (from the
fom_regex argument) matches this specific figure of merit. And the
units argument allows us to define the units on the resulting figure of
merit.
After defining this figure of merit, the:
$ ramble workspace analyze
command can be used to extract figures of merit from our experiments.
NOTE: This figure of merit definition will only extract the last line from the experiment’s output file. In the case of a parallel run, this will not contain all of the hostnames. To build this list, we would use an in-memory figure of merit, but we will leave the definition of this for a later tutorial.
Success Criteria
To help users know if their experiments worked or not, application developers can define success criteria. Success criteria can examine several aspects of an experiment, including checking for existence of (or non-existence) of a particular string, comparing the value of a figure of merit, or executing arbitrary python to determine if the experiment succeeded or failed.
For this tutorial, we will add a basic success criteria that just ensures
something was written from the hostname executable. This success criteria
should look like the following:
success_criteria("wrote_anything", mode="string", match=r".*")
Putting it all together
At this point, we should have a fairly complete hostname application
definition that includes workloads for running locally on a given machine, and
in parallel on many different machines. Users should also get reasonable
figures of merit as output, and their experiments should inform them of failed
runs.
Application Definition
Our complete application definition at this point is as follows:
from ramble.appkit import *
class Hostname(ExecutableApplication):
name = 'hostname'
executable(
"local-execute",
"hostname {input_arguments}",
)
workload("local", executables=["local-execute"])
executable(
"parallel-execute",
"hostname {input_arguments}",
use_mpi=True,
variables={"n_ranks": "{n_nodes}"}
)
workload("parallel", executables=["parallel-execute"])
workload_variable(
"input_arguments",
default="",
description="Arguments for executing `hostname`",
workloads=["*"]
)
figure_of_merit(
"possible hostname",
fom_regex=r"(?P<hostname>\S+)",
group_name="hostname",
units="",
)
success_criteria("wrote_anything", mode="string", match=r".*")
Final Tests
To complete this tutorial we will test the local workload to see how
everything works.
To begin with, delete the tutorial workspace, and recreate it using:
$ ramble workspace deactivate
$ rm -rf tutorial-workspace
$ ramble workspace create -d tutorial-workspace -a
Now, we can add an experiment to exercise the local workload using:
$ ramble workspace manage experiments hostname --workload-filter local -v n_ranks=1 -v n_nodes=1
The --workload-filter local arguments are added here to filter the
workloads so we only use the local workload. Now, to complete the test we can
execute:
$ ramble workspace setup
$ ramble on
$ ramble workspace analyze
The result of these commands should be the creation of a results.latest.txt
file that contains the hostname of your machine.
Summary and Final Cleanup
At this stage, you have now created a new application definition to execute the
hostname binary. You have tested it within a workspace, and have
constructed a custom object repository to create new definitions in.
To clean up your system, make sure to deactivate your workspace before trying to remove it. These steps can be completed with:
$ ramble workspace deactivate
$ rm -rf tutorial-workspace