HPC ATP Benchmark
Mix.install([
{:atp_benchmark_runner, github: "penthooose/atp_benchmark_runner", force: true}
])
1. Configure the environment (setup overlay)
# Fields are pre-filled from `.env` / `.env.example` and from the
# last session (the file only fills blank fields; path fields default to the
# Livebook session temp dir).
setup =
AtpBenchmarkRunner.prepare_livebook_setup(
env_file: Path.expand("../.env", __DIR__),
fallback_env_file: Path.expand("../.env.example", __DIR__),
submit_label: "Setup"
)
env_map = setup.env_map
env_file = setup.env_file
ssh_key_path = setup.ssh_key_path
2. Bootstrap the HPC session
# Use the preconfigured values from the §1 setup overlay directly, so this works
# on any machine / server without relying on a local `.env`. `env_file` (the
# overlay-written temp .env) supplies the remaining session settings (work/vault
# dirs, steady connection, retry, ...).
boot =
HpcConnect.bootstrap(
mode: :local,
env_file: env_file,
cluster: env_map["HPC_CONNECT_CLUSTER"] || "helma",
username: env_map["HPC_CONNECT_USERNAME"],
key_path: env_map["HPC_CONNECT_IDENTITY_FILE"]
)
session = boot.session
IO.inspect(
%{
cluster: session.cluster.name,
ssh_alias: session.ssh_alias,
username: session.username,
work_dir: session.work_dir,
vault_dir: session.vault_dir
},
label: "Resolved HPC session"
)
HpcConnect.bootstrap/1 installs the remote helper scripts and uploads
the generic bundled hpc_connect def-file set.
2b. Download problems
selected_problems = [
"ALG001+0.p",
"GRP001-0.p",
"GRP002+0.p",
"GRP003+0.p",
"GRP004+0.p",
"GRP720+1.p",
"GRP752-1.p",
"LAT001+0.p",
"PUZ001+0.p",
"REL001+0.p",
"SET001^0.p",
"SMT001+0.p",
"SYN000+0.p",
"THF001+0.p"
]
{downloaded, download_warnings} =
AtpBenchmarkRunner.download_tptp_problems!(selected_problems, force: false)
IO.puts("Downloaded #{length(downloaded)} problem(s):")
Enum.each(downloaded, fn problem ->
IO.puts(" ✅ #{problem.name} status=#{problem.expected_status} rating=#{problem.rating}")
IO.puts(" -> #{problem.path}")
end)
if download_warnings == [] do
IO.puts("No warnings.")
else
IO.puts("Warnings (#{length(download_warnings)}):")
Enum.each(download_warnings, fn w ->
IO.puts(" ⚠️ #{w.name}: #{inspect(w.reason)}")
end)
end
3. Pick provers and select problems from TPTP_ROOT
selected_provers = [
:tableaux,
:shot_tx,
:eprover,
:vampire,
:cvc5,
:zipperposition,
:leo3,
:leo2,
:lash
]
# Use the configured TPTP_ROOT (C:\tmp\tptp from .env).
# Problems can be selected by name; they resolve from the TPTP archive,
# bundled examples, or a livebook cache automatically.
problems =
AtpBenchmarkRunner.select_problems(
names: selected_problems,
limit: 10
)
IO.inspect(Enum.map(problems, &%{name: &1.name, path: &1.path}), label: "Problems")
4. Inspect the remote image prep plan
prover_structs = Enum.map(selected_provers, &AtpBenchmarkRunner.Prover.builtin!/1)
image_plan =
AtpBenchmarkRunner.image_build_plan(
session,
prover_structs
)
IO.inspect(image_plan, label: "Remote image build plan", limit: :infinity)
5. Optionally prepare ATP prover images now
prepare_images_now? = false
# One call: uploads each prover's apptainer.def and builds the remote .sif.
# force: true rebuilds even existing .sif images
# (apptainer build --force --ignore-fakeroot-command).
# build_on_login_node: true (default) builds directly on the login node;
# set false to chain all builds into a single compute-node sbatch job.
if prepare_images_now? do
built_images =
AtpBenchmarkRunner.build_prover_images!(
session,
prover_structs,
install_scripts: false,
force: false,
build_on_login_node: true
)
IO.inspect(built_images, label: "Built ATP prover images")
else
:ok
end
6. Bootstrap an HPC benchmark plan
Select the execution mode and resource strategy:
node_size controls how many CPUs of a node are requested.
Auto-detected per cluster (Helma CPU: 384/192, Fritz: 72/36, spr*: 104/52).
node_size |
--exclusive |
Use case |
|---|---|---|
:full |
Yes | Maximise per-prover throughput |
:half |
No | Share node with other jobs; efficient |
| Mode | hpc_mode |
single_node_mode |
Resource allocation |
|---|---|---|---|
| Single-node sequential (default) | :single_node |
:sequential |
All provers share 1 node. Tasks one at a time; each gets node CPUs. |
| Single-node parallel | :single_node |
:parallel |
All provers share 1 node. Tasks run concurrently (≤ max_parallel). |
| Multi-node (prover per node) | :multi_node |
— | Each prover gets its own node. |
# Single-node sequential, full node (default)
plan =
AtpBenchmarkRunner.bootstrap(
session,
selected_provers,
problems,
mode: :hpc,
hpc_mode: :single_node,
single_node_mode: :sequential,
timeout_seconds: 30,
wait_for_completion: false,
prepare_images: false,
extended_debug: false
)
IO.inspect(plan, label: "Single-node sequential, full node (default)", limit: :infinity)
The other execution modes (:half node, single-node parallel, multi-node) are
documented with ready-to-paste examples in the
cheat sheet and the
manual.
If you want the runner itself to prepare missing prover images immediately
before launch, set prepare_images: true in the benchmark plan above.
7. Submit the run and collect results
IO.puts("Starting HPC benchmark run...")
IO.puts(" Run ID: #{plan.id}")
IO.puts(" Provers: #{inspect(Enum.map(selected_provers, & &1))}")
IO.puts(" Problems: #{length(problems)}")
IO.puts(" Plan mode: #{plan.metadata[:hpc][:hpc_mode]}/#{plan.metadata[:hpc][:single_node_mode]}")
IO.puts(" Partition: #{plan.metadata[:hpc][:partition]}")
IO.puts("")
results = AtpBenchmarkRunner.run_benchmark(plan)
7b. Collect only last run's results
The run's ID is printed when section 7 starts (Run ID: ...), and the run
manifest (<run_id>.run.json) is persisted in the store the moment the run
starts. So the last run is always identifiable, even if the notebook cell
crashes or the SSH connection dies while fetching.
# results =
# try do
# AtpBenchmarkRunner.collect_last_hpc_results!(session)
# rescue
# e ->
# IO.puts("Resume failed: #{Exception.message(e)}")
# []
# end
# IO.puts("Fetched #{length(results)} results for the last submitted run")
To resume a specific run instead of the newest one:
# results = AtpBenchmarkRunner.collect_hpc_results!(session, "run_20260827_155553_1028")
8. Results table
results = binding()[:results] || []
AtpBenchmarkRunner.results_table(results, memory: true)
9. Aggregated report
report = AtpBenchmarkRunner.report(results, plan)
IO.puts("Run ID: #{report.run_id}")
IO.puts("Generated: #{report.generated_at}")
IO.puts("")
AtpBenchmarkRunner.print_report_markdown(report)
Per-prover breakdown
IO.puts("| Prover | Total | Solved | Failed | Solve Rate |")
IO.puts("|--------|------:|-------:|-------:|-----------:|")
Enum.each(report.by_prover, fn p ->
IO.puts("| #{p.prover} | #{p.total} | #{p.solved} | #{p.failed} | #{Float.round(p.solve_rate * 100, 1)}% |")
end)
Per-problem comparison
AtpBenchmarkRunner.print_per_problem(report)
Full explained results
IO.puts("Full explained results with proofs:\n")
AtpBenchmarkRunner.print_explain_full(results)
Filtered results by prover
target_prover = :vampire
AtpBenchmarkRunner.print_verbose_report(results, prover: target_prover, solved_only: true)
View a specific proof
target_prover = :vampire
target_problem = "GRP001-0"
AtpBenchmarkRunner.show_proof(results, target_prover, target_problem)
Raw prover debug output
AtpBenchmarkRunner.print_raw_output(results, :vampire, "GRP001-0")
10. Visualizations
results = binding()[:results] || []
results = if results == [], do: VisualizeSample.results(), else: results
target =
Enum.find(results, fn r ->
r.prover == :eprover and AtpBenchmarkRunner.Result.solved?(r)
end)
if target, do: AtpBenchmarkRunner.visualize(target), else: "No solved eprover result found."
10b. Proof dependency graph
For provers that emit machine-readable TPTP clause refutations (E, Vampire) this renders the proof as a dependency DAG. Other provers fall back to a pipeline diagram.
results = binding()[:results] || []
results = if results == [], do: VisualizeSample.results(), else: results
target =
Enum.find(results, fn r ->
match?({:ok, _}, AtpBenchmarkRunner.Visualize.Proof.parse(r))
end)
if target, do: AtpBenchmarkRunner.visualize_proof(target), else: "No parseable proof found."
10c. Run summary (status + wall-time chart)
results = binding()[:results] || []
results = if results == [], do: VisualizeSample.results(), else: results
case AtpBenchmarkRunner.visualize(results) do
:ok -> :ok
markdowns -> Enum.each(markdowns, &IO.puts/1)
end
10d. Report scoreboard
results = binding()[:results] || []
results = if results == [], do: VisualizeSample.results(), else: results
report = AtpBenchmarkRunner.report(results)
AtpBenchmarkRunner.visualize(report)
10e. Raw Mermaid source (copy into a markdown cell)
results = binding()[:results] || []
results = if results == [], do: VisualizeSample.results(), else: results
target =
Enum.find(results, fn r ->
match?({:ok, _}, AtpBenchmarkRunner.Visualize.Proof.parse(r))
end)
if target do
IO.puts(AtpBenchmarkRunner.Visualize.markdown(AtpBenchmarkRunner.Visualize.proof(target)))
else
IO.puts("No parseable proof; nothing to show.")
end
11. Persist results to local store
run = AtpBenchmarkRunner.new_run(
title: "HPC smoke test",
problems: problems,
provers: Enum.map(selected_provers, & &1),
walltime: "00:30:00",
problem_timeout_seconds: 30
)
paths = AtpBenchmarkRunner.persist_run!(run, results, report)
IO.puts("Run saved: #{paths.run_path}")
IO.puts("Results saved: #{paths.results_path}")
IO.puts("Report saved: #{paths.report_path}")
12. Cross-run comparison & statistics
Compares all stored runs at once. Results stay in JSON files under the store dir (no database yet, as columns may still change during development).
Ranking counts Timeout/GaveUp as real attempts (never excluded). By default
InputError/TypeError/UnsupportedLogic/Error are treated as non-attempts
(prover not applicable to that problem); override with :exclude_statuses
(pass [] to include them).
# One call: load all runs, aggregate, and print the comparison tables.
stats = AtpBenchmarkRunner.compare_all_runs()
# Livebook-friendly panel (Kino.Markdown when available).
AtpBenchmarkRunner.MultiRun.panel(stats)
# Save the comparison as a Markdown file.
path =
AtpBenchmarkRunner.save_multi_run_stats!(
Path.join(AtpBenchmarkRunner.store_dir(), "comparison.md")
)
IO.puts("Comparison saved: #{path}")
# Same-problem-set ranking + error-status control.
# only_common_problems: rank only on problems every prover ran.
# exclude_statuses: [] keeps input/type errors as real attempts.
stats_common =
AtpBenchmarkRunner.multi_run_stats(nil,
only_common_problems: true,
exclude_statuses: []
)
AtpBenchmarkRunner.MultiRun.print(stats_common)
13. Clean up the temporary SSH key (Optional)
Deletes the SSH key that was uploaded in §1 (stored temporarily), if any.
A persistent ~/.ssh key is never touched.
AtpBenchmarkRunner.cleanup_livebook_setup()