Templating and Context Reference
IOPS uses Jinja2 templating throughout the configuration to enable dynamic values, conditional logic, and computed expressions. This guide covers both the Jinja2 syntax and the context variables available in each configuration section.
Table of Contents
Overview
Template Support by Field
| Field | Jinja2 Support | File Path Support | Inline Support |
|---|---|---|---|
command.template | Yes | No | Yes |
command.env values | Yes | No | Yes |
scripts[].script_template | Yes | Yes | Yes |
scripts[].post.script | Yes | Yes | Yes |
scripts[].parser.file | Yes | No | Yes |
scripts[].parser.parser_script | No | Yes | Yes |
vars[].expr | Yes | No | Yes |
output.sink.path | Yes | No | Yes |
hooks[].command | Yes (own context, see Hooks) | No | Yes |
Standard Context Variables
These variables are available in all Jinja2 templates:
| Variable | Type | Description |
|---|---|---|
execution_id | int | Unique execution ID (1, 2, 3, …) |
repetition | int | Current repetition number (1, 2, 3, …) |
repetitions | int | Total repetitions for this test |
workdir | str | Base working directory |
log_dir | str | Logs directory path (workdir/logs) |
execution_dir | str | Per-execution directory path |
artifacts_dir | str | Per-execution artifacts folder path (<execution_dir>/<gallery.folder>, default <execution_dir>/images). Use this in script_template to write gallery images without hardcoding the folder name. Tracks reporting.gallery.folder automatically. |
os_env | dict | System environment variables (e.g., {{ os_env.HOME }}) |
All user vars | varies | All swept and derived variables by name |
Jinja2 Syntax Reference
Variable Substitution
Interpolate variables with {{ variable_name }}:
command:
template: "ior -w -b {{ block_size }}mb -t {{ transfer_size }}mb -o {{ output_file }}"
vars:
summary_file:
type: str
expr: "{{ execution_dir }}/summary_{{ execution_id }}_r{{ repetition }}.json"
System Environment Variables
Access system environment variables via os_env:
# Use system paths in commands and derived variables
command:
template: "{{ os_env.HOME }}/bin/benchmark --scratch {{ os_env.SCRATCH | default('/tmp') }}"
# Conditional based on cluster environment
scripts:
- name: "benchmark"
script_template: |
#!/bin/bash
{% if os_env.SLURM_JOB_ID is defined %}
echo "Running in SLURM job {{ os_env.SLURM_JOB_ID }}"
{% endif %}
{{ command.template }}
Conditionals
Use {% if condition %} for conditional logic:
# Basic conditional with else clause
command:
template: >
ior {% if operation == "write" %} -w {% else %} -r {% endif %}
-b {{ block_size }}mb
-o {{ output_path }}
# Complex conditions and membership tests
command:
template: >
benchmark
{% if nodes > 1 and processes_per_node >= 4 %} --parallel-mode {% endif %}
{% if filesystem in ["lustre", "gpfs"] %} --parallel-fs {% endif %}
{% if access_pattern not in ["sequential"] %} --random-io {% endif %}
--output {{ output_file }}
Available operators:
- Comparison:
==,!=,<,>,<=,>= - Logical:
and,or,not - Membership:
in,not in
Critical syntax requirement: Spaces are REQUIRED inside {% %} tags:
- Correct:
{% if condition %} - Wrong:
{%if condition%}(causes TemplateSyntaxError)
Loops
Use {% for %} to iterate over sequences:
# Iterate over a list
vars:
module_list:
type: str
expr: "{% for mod in ['mpi', 'ior', 'hdf5'] %}module load {{ mod }}; {% endfor %}"
# Iterate over a range
command:
template: >
benchmark
{% for i in range(3) %} --file{{ i }} input{{ i }}.dat {% endfor %}
Loop variables:
loop.index: Current iteration (1-indexed)loop.index0: Current iteration (0-indexed)loop.first: True on first iterationloop.last: True on last iteration
Filters
Filters transform values using the | operator:
# Default value for optional variables
command:
template: "benchmark --config {{ config_path | default('./default.conf') }}"
# String and numeric filters
vars:
uppercase_name:
type: str
expr: "{{ benchmark.name | upper }}"
filename:
type: str
expr: "{{ output_path | basename }}"
rounded_value:
type: float
expr: "{{ block_size_mb | round(2) }}"
Common filters:
| Filter | Description |
|---|---|
default(value) | Provide fallback for empty/falsy values |
upper | Convert to uppercase |
lower | Convert to lowercase |
basename | Extract filename from path |
dirname | Extract directory from path |
round(precision) | Round to N decimal places |
abs | Absolute value |
int | Convert to integer |
float | Convert to float |
string | Convert to string |
length | Get length of sequence |
Expressions
Combine variables with arithmetic and function calls:
vars:
total_processes:
type: int
expr: "{{ nodes * processes_per_node }}"
block_size_mb:
type: int
expr: "{{ (volume_size_gb * 1024) // (nodes * ppn) }}"
max_cores:
type: int
expr: "{{ max(nodes * ppn, 16) }}"
Available functions:
min(),max(): Minimum/maximum valuesabs(): Absolute valueround(): Round to nearest integerfloor(),ceil(): Floor/ceilingint(),float(): Type conversion
Context by Configuration Section
command.template
The main benchmark command template.
Jinja2 Support: Yes File Path Support: No (inline only)
Available Context:
- All standard Jinja2 context variables
- All
command.labelskeys
command:
template: >
mpirun -np {{ nodes * ppn }}
{% if use_collective %} --collective {% endif %}
./benchmark --block-size {{ block_size }}mb
--output {{ execution_dir }}/results.dat
labels:
operation: "write"
command.env
Environment variables passed to the execution. Values support Jinja2 templating.
Jinja2 Support: Yes (for values) File Path Support: No
Available Context:
- All standard Jinja2 context variables
command:
template: "./benchmark"
env:
OMP_NUM_THREADS: "{{ ppn }}"
IOPS_NODES: "{{ nodes }}"
IOPS_BLOCK_SIZE: "{{ block_size }}"
IOPS_EXEC_DIR: "{{ execution_dir }}"
MY_STATIC_VAR: "some_value"
Using in script_template:
Environment variables are available via command_env in your script template:
scripts:
- name: "benchmark"
script_template: |
#!/bin/bash
# Export all environment variables from command.env
{% for key, value in command_env.items() %}
export {{ key }}="{{ value }}"
{% endfor %}
# Run the command
{{ command.template }}
scripts[].script_template
The main execution script content.
Jinja2 Support: Yes File Path Support: Yes
Available Context:
- All standard Jinja2 context variables
command.template- The rendered commandcommand_env- Environment variables dictionarycommand_labels- Labels dictionaryvars- All variables as a dictionary
Inline Content
scripts:
- name: "ior"
script_template: |
#!/bin/bash
#SBATCH --job-name=iops_{{ execution_id }}
#SBATCH --nodes={{ nodes }}
#SBATCH --ntasks-per-node={{ ppn }}
module load mpi ior
srun {{ command.template }}
External File
You can reference an external file instead of inline content, either absolute or relative to the YAML config file:
scripts:
- name: "ior"
script_template: ./templates/slurm_template.sh
The external file can contain the same Jinja2 templates as inline content.
File Detection Logic
IOPS determines if the value is a file path using these rules:
- Single line (no newlines)
- Not too many
{characters (to distinguish from inline Jinja2) - File exists (checked relative to config directory first, then as absolute path)
If the file doesn’t exist, the value is treated as inline content.
scripts[].post.script
Optional post-processing script executed after the main script completes.
Jinja2 Support: Yes File Path Support: Yes
Available Context:
Same as script_template:
- All standard Jinja2 context variables
command.template,command_env,command_labels,vars
scripts:
- name: "benchmark"
script_template: |
#!/bin/bash
{{ command.template }}
post:
script: |
#!/bin/bash
echo "Execution {{ execution_id }} completed at $(date)"
echo "Output directory: {{ execution_dir }}"
ls -lh {{ execution_dir }}
Like script_template, the value can also be a path to an external file (e.g., script: ./templates/post_process.sh).
scripts[].parser.file
Path to the output file that the parser should read.
Jinja2 Support: Yes File Path Support: N/A (this IS a file path)
Available Context:
- All standard Jinja2 context variables
parser:
file: "{{ execution_dir }}/output.json"
# or
file: "{{ workdir }}/run_{{ execution_id }}/results_{{ repetition }}.txt"
scripts[].parser.parser_script
Python script that extracts metrics from the output file.
Jinja2 Support: No File Path Support: Yes
The parser script does NOT support Jinja2 templating, but has access to execution context via Python global variables:
| Variable | Type | Description |
|---|---|---|
vars | dict | All execution variables (e.g., vars["nodes"]) |
env | dict | Rendered command.env variables |
os_env | dict | System environment variables (e.g., os_env["PATH"]) |
execution_id | str | The execution ID |
execution_dir | str | The execution directory path |
workdir | str | The root working directory path |
log_dir | str | The logs directory path |
repetition | int | Current repetition number |
repetitions | int | Total number of repetitions |
Using Context Variables
Access execution context via global variables:
parser:
file: "{{ execution_dir }}/output.json"
metrics:
- name: throughput_per_node
parser_script: |
import json
def parse(file_path):
with open(file_path) as f:
data = json.load(f)
# Normalize by number of nodes
return {"throughput_per_node": data["total_bw"] / vars["nodes"]}
More patterns (regex parsing, conditional logic, CSV output, os_env usage) are covered in Writing Parsers.
External File
parser:
file: "{{ execution_dir }}/output.json"
metrics:
- name: throughput
parser_script: ./parsers/my_parser.py
The external file has the same structure and access to the same context globals.
Requirements
- Must define a function named
parse - Function takes one argument:
file_path(str) - Returns a dict with keys matching the
metricsnames
vars[].expr
Expression for derived (computed) variables.
Jinja2 Support: Yes File Path Support: No
Available Context:
- All standard Jinja2 context variables
- All previously defined variables (order matters!)
- Built-in functions:
min(),max(),abs(),round(),floor(),ceil()
Arithmetic expressions
vars:
nodes:
type: int
sweep:
mode: list
values: [1, 2, 4]
ppn:
type: int
sweep:
mode: list
values: [4, 8]
total_processes:
type: int
expr: "{{ nodes * ppn }}"
block_size_per_process:
type: int
expr: "{{ (total_volume_gb * 1024) // total_processes }}"
String templates
vars:
output_file:
type: str
expr: "{{ execution_dir }}/output_{{ execution_id }}_r{{ repetition }}.json"
job_name:
type: str
expr: "benchmark_n{{ nodes }}_p{{ ppn }}"
Conditional expressions
vars:
partition:
type: str
expr: "{% if nodes > 4 %}large{% else %}small{% endif %}"
# Environment variables work here too
scratch_path:
type: str
expr: "{{ os_env.SCRATCH | default('/tmp') }}/iops_{{ execution_id }}"
List variables for correlated parameters
The list type defines arrays that can be indexed in templates, for parameters that must change together rather than combine. For example, sweep over simulation configurations where grid size and time steps are paired:
vars:
# Sweep over an index
config_index:
type: int
sweep:
mode: list
values: [0, 1, 2, 3, 4]
# Define correlated parameter lists
grid_sizes:
type: list
expr: "[100, 200, 400, 800, 1600]"
time_steps:
type: list
expr: "[1000, 2000, 4000, 8000, 16000]"
command:
template: >
simulation --grid {{ grid_sizes[config_index] }}
--steps {{ time_steps[config_index] }}
This creates 5 executions with paired values (100/1000, 200/2000, …, 1600/16000). Sweeping the two lists as separate variables would create 25 executions (all combinations).
output.sink.path
Path to the output results file.
Jinja2 Support: Yes File Path Support: N/A (this IS a file path)
Available Context:
workdir- Base working directory- Other standard context variables
output:
sink:
type: parquet
path: "{{ workdir }}/results/{{ benchmark.name }}_results.parquet"
If path is not specified, IOPS uses per-type defaults; see YAML Schema / output.
Complete Example
Here’s a complete configuration demonstrating all script types and their context:
benchmark:
name: "IO Benchmark"
workdir: "/scratch/benchmarks"
executor: "slurm"
repetitions: 3
vars:
nodes:
type: int
sweep:
mode: list
values: [1, 2, 4]
ppn:
type: int
sweep:
mode: list
values: [4, 8]
# Derived variable using Jinja2
total_procs:
type: int
expr: "{{ nodes * ppn }}"
# Path variable using Jinja2
summary_file:
type: str
expr: "{{ execution_dir }}/summary.json"
command:
template: >
ior -w -r -b 1g -t 1m
-O summaryFile={{ summary_file }}
# Environment variables with Jinja2
env:
IOPS_NODES: "{{ nodes }}"
IOPS_PPN: "{{ ppn }}"
IOPS_TOTAL: "{{ total_procs }}"
labels:
operation: "write-read"
scripts:
- name: "ior"
# Can be inline or file path - Jinja2 supported
script_template: |
#!/bin/bash
#SBATCH --job-name=iops_{{ execution_id }}
#SBATCH --nodes={{ nodes }}
#SBATCH --ntasks-per-node={{ ppn }}
#SBATCH --output={{ execution_dir }}/stdout
# Export command.env variables
{% for key, value in command_env.items() %}
export {{ key }}="{{ value }}"
{% endfor %}
module load mpi ior
srun {{ command.template }}
# Optional post script - Jinja2 supported
post:
script: |
#!/bin/bash
echo "Completed: {{ execution_id }}"
parser:
# File path with Jinja2
file: "{{ summary_file }}"
metrics:
- name: write_bw
- name: read_bw
- name: efficiency
# No Jinja2, but has access to vars, env, os_env, execution_id, execution_dir, workdir, log_dir, repetition, repetitions
parser_script: |
import json
def parse(file_path):
# Access context variables
nodes = vars["nodes"]
ppn = vars["ppn"]
with open(file_path) as f:
data = json.load(f)
write_bw = data["tests"][0]["Results"][0]["bwMiB"]
read_bw = data["tests"][1]["Results"][0]["bwMiB"]
return {
"write_bw": write_bw,
"read_bw": read_bw,
"efficiency": write_bw / (nodes * ppn)
}
output:
sink:
type: csv
path: "{{ workdir }}/results.csv"