🐏 RamParILS
Give it a parameter space, a set of instances, and a time budget. Get back a faster or lower-cost algorithm configuration.
RamParILS is a parallel Rust implementation of ParamILS, an automatic algorithm configurator based on Iterated Local Search. It searches discrete parameter spaces, evaluates candidate configurations concurrently, and can keep every result in a persistent SQLite cache.
RamParILS is used as the inner tuner in Grackle, but it is designed to work with any command-line algorithm that can follow the small wrapper protocol described below.
💡 The idea
Many solvers and heuristic programs expose dozens of options. Choosing them by hand is slow, benchmark-specific, and often surprising. RamParILS automates that work:
- You describe the legal parameter values in a ParamILS-compatible file.
- You list representative training instances.
- A wrapper runs your target algorithm and reports runtime or solution quality.
- RamParILS explores configurations until the tuning budget expires and prints the best one found.
FocusedILS is the default search method. Promising configurations are tested on more instances while poor candidates are rejected early. Independent solver runs are distributed across a bounded worker pool, and adaptive capping avoids spending the full cutoff on configurations that are already losing.
🚀 Why RamParILS?
| Original Ruby ParamILS | RamParILS | |
|---|---|---|
| Candidate evaluation | Sequential | Parallel over all (neighbour, instance) pairs |
| Result cache | In-memory, per run | Persistent SQLite, shared across runs, self-describing |
| Cache inspection | — | ramparils db solved | status | confs |
| Integration | Command line | Command line and native Python extension (PyO3) |
| Search modes | BasicILS, FocusedILS | BasicILS, FocusedILS, random (ParamILS's pert_rand) |
| Escaping a local optimum | Random restart at fixed probability (p_restart), R random probes |
Both, plus soft acceptance within a tolerance and stagnation-triggered restarts |
| Restart target | Uniformly random configuration | Random, or a bounded perturbation of the incumbent |
| Comparing across fidelities | Score vector per configuration, compared at a common level | Single score, re-measured for the incumbent and the home base at every fidelity increase |
| Multi-phase schedules | — | Iterative deepening: geometric growth of instances, cutoff and deadline |
| Adaptive capping | Yes | Yes, with the ceiling rule documented — see Algorithm |
| Provenance | — | Source revision in --version and in every log header; full scenario echoed at startup |
| Multiple random seeds | Supported | Not currently supported |
Parallel evaluation is the primary motivation for the rewrite. The actual speedup depends on worker count, neighbourhood width, current fidelity, solver runtimes, early acceptance, and cache hits.
RamParILS currently assumes that the target algorithm is deterministic. Cache entries are keyed only by the active configuration and instance path. The algorithm command, cutoff, objective, solver version, and random seed are not part of the key. Use a separate cache whenever any of those change.
🖼️ What a run looks like
approach: basic — the variant that evaluates every candidate on the whole
instance set, so all scores are directly comparable. focused wraps the same
loop in a growing instance prefix; see Algorithm.
Three configurations are in play at once, and keeping them apart is most of
understanding the search: θ the current candidate, θ_base the point
each perturbation starts from, and θ_inc the incumbent — the best seen, and
what the run returns. Only θ_base is perturbed, so once it stops moving the
search degenerates into repeated sampling from a fixed ball no matter what the
incumbent does. That is what acceptance_tolerance and the restart triggers
above exist to prevent.
📋 Requirements
- Rust 1.85 or newer for the CLI
- Python 3.9 or newer for the Python extension
- A Unix-like system for solver process-group and signal handling
- A target algorithm or wrapper that implements the RamParILS result protocol
⚙️ Installation
🦀 Command-line tools
Build the ramparils binary with Cargo:
git clone https://github.com/deeper4ai/ramparils.git
cd ramparils
cargo build --release
The executables are written to:
target/release/ramparils
Install Rust with rustup if cargo is not available.
🐍 Python extension
Install the published package:
python -m pip install ramparils
For local development, create or activate a virtual environment and run:
python -m pip install maturin
maturin develop
See the full installation guide for more detail.
⚡ Quick start
A tuning job needs four small pieces.
1️⃣ Describe the parameters
Create solver.params:
# name {allowed values} [default]
engine {quick, thorough} [quick]
threads {1, 2, 4, 8} [4]
restart {none, geometric} [geometric]
factor {1.2, 1.5, 2.0} [1.5] | restart in {geometric}
# This combination must never be evaluated.
{engine=thorough, threads=1}
Values are passed as strings. Parameters may be conditional, and forbidden combinations are excluded from the search. The syntax is documented in Parameter files.
2️⃣ List training instances
Create instances.txt, with one path per line:
benchmarks/easy-01.in
benchmarks/easy-02.in
benchmarks/hard-01.in
Blank lines and lines beginning with # are ignored. Paths are interpreted by
the wrapper, so running from a predictable working directory is recommended.
3️⃣ Write a solver wrapper
RamParILS invokes the configured command as:
<algo> <instance> <cutoff_time> -parameter value ...
The wrapper may print normal diagnostic output, but it must include one result line on stdout:
#%# RamParIls #%# <status>, <runtime>, <quality>
For example:
#%# RamParIls #%# OK, 1.237, 0.0
status is preserved as text, runtime is measured in seconds, and quality
is used for quality-oriented scenarios. Missing or malformed results are
penalized. See the wrapper protocol for a minimal
wrapper and failure behavior.
4️⃣ Create and run a scenario
Create scenario.yaml:
algo: "python3 solver_wrapper.py"
paramfile: "solver.params"
instance_file: "instances.txt"
cutoff_time: 10.0
tuner_timeout: 300.0
run_obj: runtime
overall_obj: mean
approach: focused
cores: 0 # 0 uses all available CPU cores
cache_db: "results.dbcache" # reuse evaluations in later runs
debug: true
Then run:
ramparils run scenario.yaml
The complete best configuration is printed to stdout as YAML:
engine: quick
factor: '1.2'
restart: geometric
threads: '8'
All tuning options live in the scenario file, keeping runs reproducible and easy to share. The CLI guide lists every field and its default.
🐍 Python API
The Python extension runs the same Rust implementation in-process. Pass an initial strategy and a scenario dictionary:
import ramparils
best = ramparils.specialize(
strategy={
"engine": "quick",
"threads": "4",
"restart": "geometric",
"factor": "1.5",
},
scenario={
"algo": "python3 solver_wrapper.py",
"paramfile": "solver.params",
"instances": [
"benchmarks/easy-01.in",
"benchmarks/easy-02.in",
"benchmarks/hard-01.in",
],
"cutoff_time": 10.0,
"tuner_timeout": 300.0,
"cores": 0,
"cache_db": "results.dbcache",
},
)
print(best)
The result is a dict[str, str] containing the best active parameters. The
initial strategy must provide every parameter from the parameter file,
including parameters that may become inactive. See the
Python API guide and
examples/saps_python.py.
🧭 Scenario essentials
| Field | Purpose |
|---|---|
algo |
Shell command for the target algorithm or wrapper |
paramfile |
ParamILS-compatible parameter-space file |
initial_config / initial_config_file |
Optional complete ILS starting configuration, inline or in a YAML file |
instance_file / instances |
Training instances; use exactly one |
cutoff_time |
Per-solver-run limit in seconds |
tuner_timeout |
Total tuning budget in seconds |
run_obj |
Minimize runtime or the numeric quality cost |
overall_obj |
Aggregate results with mean or median |
approach |
focused, basic, or random |
perturbation_strength |
Parameters changed per perturbation; ParamILS's rule is max(2, ceil(0.2 × parameters)) |
acceptance_tolerance |
Accept a home base within this relative margin of the incumbent; 0 keeps the ParamILS rule |
restart_failures / restart_probability |
Restart after k rejected local optima, or with a fixed probability per round |
restart_target / restart_strength |
Where a restart lands, and how far it jumps |
cores |
Worker count; 0 selects all available cores |
cache_db |
SQLite cache path; defaults to :memory: |
Useful optional controls include iterative deepening, initial evaluation fidelity, adaptive capping, random probes, debug traces, and separate crash logs. They are all covered in the CLI scenario reference.
🗄️ Exporting the cache
ramparils db writes out what an existing cache holds, without running the
tuner. All three sub-commands produce one file per strategy hash, named
ram-<hash>, under a layout that mirrors solverpy's database — so an export
can be dropped straight into an existing solverpy_db/:
# All three at once.
ramparils db results.dbcache
# Or one at a time:
# Instances each strategy solved, one path per line.
ramparils db solved results.dbcache # -> solverpy_db/solved/results/ram-<hash>
# Every result, as instance <TAB> status <TAB> runtime.
ramparils db status results.dbcache # -> solverpy_db/status/results/ram-<hash>
# The configuration behind each hash, as YAML.
ramparils db confs results.dbcache # -> solverpy_db/confs/results/ram-<hash>
ramparils db confs results.dbcache --json
--out-dir moves the root; it defaults to solverpy_db. Each command prints a
one-line summary on stdout and uses stderr for errors only.
Results are keyed by a hash of the active configuration, and the cache records
what each hash means in a strategies table, written the first time a
configuration is evaluated. Without it a .dbcache is a pile of opaque hashes:
recovering them would depend on the parameter space still being small enough to
enumerate and on the hash being reproducible by the current toolchain — and
hash_config uses DefaultHasher, which is explicitly not portable across
compiler versions.
Opening a cache written before the table existed adds it automatically;
existing results stay usable, but their hashes are only described from that
point on. ramparils db confs says so rather than writing an empty directory.
confs/ is not solverpy's strats/. A strategy file there holds a solver
command line; a conf file here holds a parameter assignment, which only means
anything against the parameter space it was tuned in. It also records the
active configuration — parameters whose guard was closed are absent, since
that is exactly what the cache keys on — so it is a record of what ran rather
than a complete configuration, and initial_config_file will reject it unless
every parameter happened to be active.
The cache is most valuable when several tuning runs use exactly the same solver semantics. Keep separate cache files when the algorithm command, cutoff, objective, solver version, wrapper behavior, or benchmark meaning changes.
🧪 Examples
examples/primo: tune theprimoQF_LRA SMT solver. The most developed example — a 24-parameter conditional space with the evidence for each choice recorded inline, a BasicILS scenario whose knobs come from a nine-run campaign, and a header comment on what to adjust firstexamples/llm2smt: tune an SMT solver with a standalone Python wrapperexamples/eprover: specialize E prover strategies for Grackleexamples/saps_python.py: call RamParILS through Python
🛠️ Development
cargo test
cargo fmt --check
cargo clippy --all-targets --all-features
cargo build --release
./docs-build.sh
The E prover integration test requires external executables and may be slower than the unit tests.
Preview the documentation with ./docs-serve.sh. Maintainers can publish a
clean, committed tree to
GitHub Pages with
./docs-deploy.sh.
📚 Documentation
- Changelog
- Installation
- CLI and scenario files
- Python API
- Search algorithm
- Parameter-file syntax
- Solver wrapper protocol
- Glossary
🤝 Acknowledgements
RamParILS is part of DEEPER and is supported by the DEEPER grant from Renaissance Philanthropy.
Metadata
Release files for ramparils 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ramparils-0.2.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | CPython 3.9 | abi3 | Linux glibc 2.17+ x86-64 | Details |
Release files / ramparils-0.2.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | ramparils-0.2.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 1.4 MB |
| Tags | CPython 3.9 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
e74ce627ac2063d036c3df3d026d000817bc6ebe609f79c13c406f52787c54cc
|
|
BLAKE2b-256 checksum How to use checksums |
8dd3ab9baa658a8649e0e7414f33939a7a0dc67c9136f31153da7f9cc5ba1572
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.6
|