Powered by AppSignal & Oban Pro

HPC Benchmark

examples/benchmark_hpc.livemd

HPC Benchmark

Mix.install([
  {:atp_benchmark_runner, path: "item_14_ATP_Benchmark_Runner/atp_benchmark_runner", force: true}
])

1. Configure paths from .env

project_root = Path.expand("..", __DIR__)
env_file = Path.join(project_root, ".env")

# Load .env values into System.put_env so Config.get() picks them up
env_map = HpcConnect.load_env_file(env_file)
Enum.each(env_map, fn {k, v} -> System.put_env(k, v) end)

IO.puts("Configured paths:")
IO.puts("  TPTP_ROOT:      #{AtpBenchmarkRunner.tptp_dir()}")
IO.puts("  Store dir:      #{AtpBenchmarkRunner.store_dir()}")
IO.puts("  SMT temp dir:   #{AtpBenchmarkRunner.Config.smt_tmp_dir()}")
IO.puts("  .env file:      #{env_file}")

This notebook is the HPC counterpart to the local example. It uses the local workspace versions of both libraries because atp_benchmark_runner depends on the sibling hpc_connect path from its mix.exs.


2. Bootstrap the HPC session

boot =
    HpcConnect.bootstrap(
        mode: :local,
        env_file: env_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,
   :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

if prepare_images_now? do
    uploaded_defs =
        AtpBenchmarkRunner.upload_prover_definitions!(
            session,
            prover_structs,
            install_scripts: false
        )

    IO.inspect(uploaded_defs, label: "Uploaded ATP prover defs")

    built_images =
        AtpBenchmarkRunner.build_prover_images!(
            session,
            prover_structs,
            install_scripts: false
        )

    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
    )

IO.inspect(plan, label: "Single-node sequential, full node (default)", limit: :infinity)
# ── Single-node sequential, half node ────────────────────────
plan_half =
    AtpBenchmarkRunner.bootstrap(
        session,
        selected_provers,
        problems,
        mode: :hpc,
        hpc_mode: :single_node,
        single_node_mode: :sequential,
        node_size: :half,
        timeout_seconds: 30,
        wait_for_completion: false,
        prepare_images: false
    )

IO.inspect(plan_half, label: "Single-node sequential, half node", limit: :infinity)
# ── Single-node parallel, full node ──────────────────────────
plan_parallel =
    AtpBenchmarkRunner.bootstrap(
        session,
        selected_provers,
        problems,
        mode: :hpc,
        hpc_mode: :single_node,
        single_node_mode: :parallel,
        max_parallel_jobs: 4,
        timeout_seconds: 30,
        wait_for_completion: false,
        prepare_images: false
    )

IO.inspect(plan_parallel, label: "Single-node parallel plan", limit: :infinity)
# ── Multi-node ────────────────────────────────────────────────
# Each prover gets its own node (default full).
plan_multi =
    AtpBenchmarkRunner.bootstrap(
        session,
        selected_provers,
        problems,
        mode: :hpc,
        hpc_mode: :multi_node,
        timeout_seconds: 30,
        wait_for_completion: false,
        prepare_images: false
    )

IO.inspect(plan_multi, label: "Multi-node plan", limit: :infinity)

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("  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("")

{t_ms, results} =
    :timer.tc(fn ->
        AtpBenchmarkRunner.run_benchmark(plan)
    end)

elapsed_s = t_ms / 1_000_000
IO.puts("Benchmark completed in #{Float.round(elapsed_s, 2)}s")
IO.puts("Total results: #{length(results)}")

8. Results table

IO.puts("| # | Problem | Prover | SZS Status | Wall (ms) | Memory (KB) | Solved? |")
IO.puts("|---|---------|--------|------------|-----------|-------------|---------|")

results
|> Enum.with_index(1)
|> Enum.each(fn {r, i} ->
    solved = if AtpBenchmarkRunner.Result.solved?(r), do: "✅", else: "❌"
    IO.puts("| #{i} | #{r.problem_id} | #{r.prover} | #{r.szs_status || "?"} | #{r.wall_time_ms || "?"} | #{r.memory_kb || "?"} | #{solved} |")
end)

9. Aggregated report

report = AtpBenchmarkRunner.report(results, plan)

IO.puts("Run ID: #{report.run_id}")
IO.puts("Generated: #{report.generated_at}")
IO.puts("")
IO.puts(report.markdown)

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.explain_full(results) |> IO.puts()

Filtered results by prover

target_prover = :vampire

IO.puts("Verbose report for prover #{target_prover}:\n")
AtpBenchmarkRunner.verbose_report(results, prover: target_prover, solved_only: true)
|> Enum.each(&IO.puts/1)

View a specific proof

target_prover = :vampire
target_problem = "GRP001-0"

AtpBenchmarkRunner.show_proof(results, target_prover, target_problem)

Raw prover debug output

target_prover = :vampire
target_problem = "GRP001-0"

result = Enum.find(results, fn r ->
  r.prover == target_prover and r.problem_id == target_problem
end)

if result && result.raw_output do
  IO.puts("Raw output for #{result.problem_id} / #{result.prover}:\n")
  IO.puts(result.raw_output)
else
  IO.puts("Result not found or no raw output stored (rerun with include_raw_output: true)")
end

10. 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
)

run_path = AtpBenchmarkRunner.save_run!(run)
IO.puts("Run saved: #{run_path}")

results_path = AtpBenchmarkRunner.save_results!(run, results)
IO.puts("Results saved: #{results_path}")

report_path = AtpBenchmarkRunner.save_report!(run, report)
IO.puts("Report saved: #{report_path}")