Caching
Table of Contents
Overview
IOPS caches execution results in SQLite so that tests with identical parameters can be skipped on re-runs. This speeds up iterative development, lets you safely retry failed runs without re-executing successful tests, and handles repetitions correctly (each repetition is cached separately).
Warning: Use caching with caution. The cache key is computed only from variable values, not from the command template. If you change your command (e.g., from ./benchmark {{ var1 }} to ./benchmark -F {{ var1 }}), the cache will still return old results because the parameters haven’t changed. This can lead to incorrect results. See Command Changes Are Not Detected for details and workarounds.
Configuration
Add cache_file to your YAML config:
benchmark:
name: "IOR Benchmark"
workdir: "/home/user/workdir/"
cache_file: "/home/user/iops_cache.db" # Cache database
repetitions: 3
executor: "local"
Basic Usage
# First run: executes all tests and caches results
iops run config.yaml
# Second run with --use-cache: reuses cached results
iops run config.yaml --use-cache
Cache-Only Mode
The --cache-only flag runs IOPS using only cached results, skipping any tests not found in the cache. Use it to regenerate CSV/output files from an existing cache, extract a subset of results from a large campaign, or verify what’s in the cache without executing anything.
# Only use cached results, skip tests not in cache
iops run config.yaml --cache-only
# Preview what would be cached vs skipped
iops run config.yaml --cache-only --dry-run
Tests found in cache are loaded and written to the output file. Tests not in cache are marked as SKIPPED with reason “Not in cache (cache-only mode)” and appear in iops find --status SKIPPED. The final summary shows how many tests came from cache vs were skipped:
[ 1] exec_0001 (rep 1/1) → SUCCEEDED [CACHED] | result=100
[ 2] exec_0002 (rep 1/1) -> SKIPPED (not in cache)
Benchmark completed (cache-only mode): 3 tests processed
From cache: 2
Skipped (not in cache): 1
Note: --cache-only requires benchmark.cache_file to be configured in the YAML file. Using --cache-only automatically enables --use-cache.
Parameter Hashing
Each test is uniquely identified by:
- Parameters: All swept and fixed variables (e.g.,
nodes=2, processes=16) - Repetition: Which repetition this is (1, 2, 3, …)
Parameters are normalized before hashing:
- Numeric strings: a string is coerced to a number only when the numeric form round-trips back to the exact original string.
"8"and8hash the same, but"1.10"and"1.1"(or"08"and"8","1e3"and"1000.0") keep distinct hashes. - Internal keys removed:
__test_index, etc. are ignored - Excluded variables removed: Variables in
cache_exclude_varsare skipped - Sorted consistently: Order doesn’t matter
Excluding Variables from Cache
Some derived variables contain run-specific paths that change between executions, causing unnecessary cache misses:
vars:
nodes:
type: int
sweep: { mode: list, values: [2, 4, 8] }
summary_file:
type: str
expr: "{{ execution_dir }}/summary.txt" # Contains run_001, run_002, etc.
Without exclusion:
- Run 1:
summary_file=/workdir/run_001/summary.txt→ Cache key:abc123 - Run 2:
summary_file=/workdir/run_002/summary.txt→ Cache key:def456Cache miss!
Use cache_exclude_vars to exclude path-based variables:
benchmark:
cache_file: "/home/user/iops_cache.db"
cache_exclude_vars: ["summary_file"] # Exclude from cache hash
Cache Schema
CREATE TABLE cached_executions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
param_hash TEXT NOT NULL, -- MD5 hash of normalized params
params_json TEXT NOT NULL, -- Full parameters as JSON
repetition INTEGER NOT NULL, -- 1, 2, 3, ...
metrics_json TEXT, -- Benchmark metrics
metadata_json TEXT, -- Execution metadata
created_at TEXT NOT NULL, -- Timestamp
UNIQUE(param_hash, repetition)
);
-- Index for fast lookups
CREATE INDEX idx_param_hash ON cached_executions(param_hash, repetition);
Important: Command Changes Are Not Detected
Warning: The cache hash is computed only from variables, not from the command template itself. This is intentional: commands often contain paths like {{ execution_dir }} that change between runs but shouldn’t invalidate the cache.
However, this means that if you modify your command template in ways that affect results, the cache will still return old results:
# Original command
command:
template: "fio --name=test --rw=read --bs={{ block_size }}"
# Modified command (added --direct=1 for unbuffered I/O)
command:
template: "fio --name=test --rw=read --bs={{ block_size }} --direct=1"
Since block_size hasn’t changed, the cache hash remains the same, and you’ll get stale results that don’t reflect the --direct=1 behavior.
Solutions:
- Clear the cache when changing command behavior: delete the cache file or use
cache.clear_cache() - Add a version variable to force cache invalidation:
vars: cache_version: type: int value: 2 # Increment when command changes - Use a new cache file for different command configurations
What Gets Cached
Cached:
- Execution metrics (e.g.,
bwMiB,iops,latency) - Execution metadata (status, timestamps, job IDs)
- Only
STATUS_SUCCEEDEDexecutions
Not Cached:
- Failed executions (
STATUS_FAILED,STATUS_ERROR) - Script files (regenerated each time)
- stdout/stderr file contents (only exist for executed tests)
- Log files
Inspecting the Cache
IOPS provides three read-only subcommands to explore what is stored in a cache database.
iops cache stats
Print a summary of the cache: total entries, unique parameter sets, and date range.
$ iops cache stats cache.db
Cache: cache.db
--------------------------------------------------
Total entries: 28
Unique parameter sets: 14
Distinct repetitions: 2
Oldest entry: 2026-04-06T18:58:32.285294
Newest entry: 2026-04-17T23:53:07.035720
iops cache list
List cached entries, collapsed per unique parameter hash. Each row shows a short (8-character) hash, the parameter values, the number of cached repetitions, and metric averages across repetitions.
$ iops cache list cache.db
Hash dataset light_conv Reps average_epoch_time_sec peak_gpu_memory_mib Last Cached
-----------------------------------------------------------------------------------------------------
57c77ec3 moving_mnist tied 2 430.72 10996.97 2026-04-17T23:53:07
096cd29f taasrad19 sqnxt 2 391.29 11134.85 2026-04-17T12:21:38
...
Filter by parameter values using the same VAR=VALUE syntax as iops find:
# Only entries where dataset=moving_mnist
iops cache list cache.db dataset=moving_mnist
# Hide metric columns, keep just params and repetition counts
iops cache list cache.db --no-metrics
# Show full parameter values without truncation
iops cache list cache.db --full
# Limit to the 10 most recently cached parameter sets
iops cache list cache.db --limit 10
# Emit JSON for scripting
iops cache list cache.db --json
iops cache show
Show full details for a single cached parameter set, including every repetition’s metrics and metadata. The hash argument accepts a git-style short prefix:
$ iops cache show cache.db 57c77e
Hash: 57c77ec316e9b66f3aa9861227fa1a83
Short: 57c77ec3
Parameters:
dataset: moving_mnist
light_conv: tied
Repetitions: 2
rep=1 cached_at=2026-04-08T08:01:27.580146
metrics:
average_epoch_time_sec: 428.96
...
metadata:
__executor_status: SUCCEEDED
...
If the prefix matches multiple hashes, IOPS reports the candidates and asks for a longer prefix:
$ iops cache show cache.db 5
ERROR | Hash prefix '5' is ambiguous: matches 2 entries (57c77ec3, 58abbe6f). Provide a longer prefix.
Use --full to disable truncation of long values and --json for machine-readable output.
Python API
The CLI subcommands are thin wrappers around helpers in iops.cache that you can call directly:
from iops.cache import (
list_cache_entries, get_cache_entry, get_cache_stats,
resolve_hash_prefix, ExecutionCache,
)
stats = get_cache_stats("cache.db") # total_entries, unique_parameter_sets, ...
# List entries, filtered and collapsed per parameter hash
entries = list_cache_entries("cache.db", param_filters={"dataset": "moving_mnist"}, limit=10)
# Full detail for a single entry (git-style prefix accepted)
entry = get_cache_entry("cache.db", "57c77e")
# Resolve a prefix to a full hash (raises HashPrefixError on ambiguity)
full_hash = resolve_hash_prefix("cache.db", "57c77e")
# Mutating operations still go through ExecutionCache
ExecutionCache("cache.db").clear_cache()
Rebuilding the Cache
The iops cache rebuild command modifies an existing cache database by:
- Excluding variables from the hash (consolidating entries that now hash to the same value)
- Adding variables with constant values to all entries (enabling cache reuse after config changes)
Usage
# Exclude variables from hash
iops cache rebuild cache.db --exclude summary_file,output_path -o new_cache.db
# Add a variable to all entries (with type); repeat --add for multiple variables
iops cache rebuild cache.db --add use_new_flag:bool=false -o new_cache.db
# Combine exclude and add
iops cache rebuild cache.db --exclude output_path --add use_feature:bool=true -o new_cache.db
Adding Variables (Type Syntax)
When adding variables, specify the type to ensure it matches your YAML config:
--add VAR:TYPE=VALUE
| Type | Examples | Notes |
|---|---|---|
str | --add label=test | Default if no type specified |
int | --add count:int=10 | |
float | --add rate:float=1.5 | |
bool | --add flag:bool=false | Accepts: true/false, yes/no, 1/0 |
Why types matter: The cache hash depends on both the value and its type. If your YAML defines use_feature: { type: bool, ... }, you must add it as --add use_feature:bool=false (not just --add use_feature=false) for the hashes to match.
Example: Excluding Path Variables
You ran 100 executions but forgot to exclude a path-based variable:
vars:
nodes:
type: int
sweep: { mode: list, values: [2, 4, 8] }
output_file:
type: str
expr: "{{ execution_dir }}/results.txt" # Oops! Should have been excluded
The cache has 100 unique hashes because output_file differs for each execution. After rebuilding:
$ iops cache rebuild cache.db --exclude output_file
Cache Rebuild Summary
==================================================
Source entries: 100
Source unique hashes: 100
Output unique hashes: 3
Collapsed entries: 97
==================================================
Rebuilt cache saved to: cache_rebuilt.db
Example: Adding a New Variable
You have a cache from previous runs and add a new swept variable to your config:
vars:
nodes:
type: int
sweep: { mode: list, values: [2, 4, 8] }
use_new_optimization: # New variable, not in the cache
type: bool
sweep: { mode: list, values: [false, true] }
To reuse the existing cache for use_new_optimization=false cases:
iops cache rebuild cache.db --add use_new_optimization:bool=false -o new_cache.db
Now iops run new_config.yaml --use-cache will find cache hits for use_new_optimization=false and only execute the use_new_optimization=true cases.
How It Works
Rebuild reads all entries from the source cache, adds new variables to each entry’s parameters (--add), re-normalizes parameters excluding specified variables (--exclude), re-computes each hash, and writes all entries to the new database.
Important: When multiple entries collapse to the same (hash, repetition), all entries are preserved. The rebuilt database does not enforce uniqueness, so all historical data is kept. When reading from the cache, IOPS uses ORDER BY created_at DESC LIMIT 1 to return the most recent entry.
After Rebuilding
Update your YAML config to use the rebuilt cache; future runs with --use-cache will find cache hits for matching parameters:
benchmark:
cache_file: "./workdir/new_cache.db"
cache_exclude_vars: ["output_file"] # If you excluded variables
Cache Behavior Details
When cache is used: before each test execution, IOPS checks whether (params, repetition) exists in the cache. If found, the test is populated with cached metrics and metadata and execution is skipped; otherwise it executes normally.
When cache is updated: after each successful execution (STATUS_SUCCEEDED), IOPS stores (params, repetition, metrics, metadata). On duplicate (same params/repetition), the entry is updated with the latest result.
Core-Hours Savings Tracking
When using cache with a core-hours budget (max_core_hours), cached tests don’t count toward the budget and IOPS tracks how many core-hours were saved. This appears in progress logs and the final summary.
See the Budget Control guide for details.