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

  1. Overview
  2. Jinja2 Syntax Reference
  3. Context by Configuration Section
  4. Complete Example

Overview

Template Support by Field

FieldJinja2 SupportFile Path SupportInline Support
command.templateYesNoYes
command.env valuesYesNoYes
scripts[].script_templateYesYesYes
scripts[].post.scriptYesYesYes
scripts[].parser.fileYesNoYes
scripts[].parser.parser_scriptNoYesYes
vars[].exprYesNoYes
output.sink.pathYesNoYes
hooks[].commandYes (own context, see Hooks)NoYes

Standard Context Variables

These variables are available in all Jinja2 templates:

VariableTypeDescription
execution_idintUnique execution ID (1, 2, 3, …)
repetitionintCurrent repetition number (1, 2, 3, …)
repetitionsintTotal repetitions for this test
workdirstrBase working directory
log_dirstrLogs directory path (workdir/logs)
execution_dirstrPer-execution directory path
artifacts_dirstrPer-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_envdictSystem environment variables (e.g., {{ os_env.HOME }})
All user varsvariesAll 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 iteration
  • loop.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:

FilterDescription
default(value)Provide fallback for empty/falsy values
upperConvert to uppercase
lowerConvert to lowercase
basenameExtract filename from path
dirnameExtract directory from path
round(precision)Round to N decimal places
absAbsolute value
intConvert to integer
floatConvert to float
stringConvert to string
lengthGet 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 values
  • abs(): Absolute value
  • round(): Round to nearest integer
  • floor(), ceil(): Floor/ceiling
  • int(), 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.labels keys
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 command
  • command_env - Environment variables dictionary
  • command_labels - Labels dictionary
  • vars - 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:

  1. Single line (no newlines)
  2. Not too many { characters (to distinguish from inline Jinja2)
  3. 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:

VariableTypeDescription
varsdictAll execution variables (e.g., vars["nodes"])
envdictRendered command.env variables
os_envdictSystem environment variables (e.g., os_env["PATH"])
execution_idstrThe execution ID
execution_dirstrThe execution directory path
workdirstrThe root working directory path
log_dirstrThe logs directory path
repetitionintCurrent repetition number
repetitionsintTotal 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 metrics names

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"