Skip to content

Keep SV-COMP runs within the competition's memory and CPU budget - #88

Closed
nils-loose wants to merge 10 commits into
svcomp27/integrationfrom
fix/resource-budget
Closed

nils-loose wants to merge 10 commits into
svcomp27/integrationfrom
fix/resource-budget

Conversation

@nils-loose

@nils-loose nils-loose commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Addresses #51: SV-COMP runs now stay inside the competition's limits (15 GB, 900 s CPU, 4 cores) and stop with UNKNOWN instead of being killed.

What changed and why

  • One resource budget (symbolic-explorer/svcomp/ResourceBudget.py): the explorer and every executor JVM share it. A monitor thread polls the RSS and CPU time of the explorer and its descendants every 0.2 s. Where the memory limit comes from a cgroup (BenchExec, systemd scope), the CPU time is at least the cgroup's cpu.stat.
    • Defaults are the competition limits: --memory-limit 15000 (MB, i.e. BenchExec's 15e9 bytes) and --cpu-limit 900. A lower cgroup memory.max wins. --cpu-used adds CPU time spent before the explorer started.
    • Executor heap is derived from the limit: -Xmx5483m for 15 GB, replacing -Xmx32g. JavaSMT's Z3 context grows outside the heap at about the heap's rate: with -Xmx12g the executor RSS reached 14-15 GB.
    • 2 GB below the memory limit:
      • A running executor is killed with SIGKILL (a SIGTERM would let the shutdown hook send a cut-short trace), and exploration continues with other branches.
      • A running Z3 query is interrupted. If it is still over the limit 1 s later, or within 1 GB of the limit, the run ends with UNKNOWN. Z3 ignores interrupts while bit-blasting and grew by 2 GB/s on Conflict. Z3's own memory_max_size was tried and dropped because it made Z3 abort the process.
    • 20 s before the CPU limit: exploration stops with UNKNOWN. A violation that the stopped run printed is still reported.
    • Soundness: a run that was cut short (killed by the budget, or an OutOfMemoryError in the executor) leaves its path unexplored. It is recorded as resource_limit / out_of_memory and rules out SAFE, like the existing downgrades. Its (missing) trace is not checked for divergence. stats.json gets a resources section.
  • Executor JVM uses C1 only (-XX:TieredStopAtLevel=1). Every round is a fresh JVM that runs 1-3 s. On 4 cores, C2 compiler threads took 41-44 % of the executor CPU and GC took 0.2 %. Same 120 s CPU: 22-53 % more rounds (MinePump 44→63, TCAS 59→90, Ramp-and-Hold 49→60). SerialGC and -Xint did not help.
  • Competition package:
    • run_swat.py passes the CPU time used by copying and compiling the task.
    • run_swat.py no longer buffers and re-logs the explorer output. After 900 s that took over 20 s of CPU, so BenchExec still reported TIMEOUT, and it held hundreds of MB in memory. The output is now passed straight through, and the verdict for printing the witness is read from stats.json.
    • In sv-comp mode, the root logger stays at WARNING (the module DEBUG/INFO lines were 500 MB of stdout in 900 s).
    • The per-node [DFS] line now logs below DEBUG (270 MB of explorer.log).
    • BenchExec containers keep both in RAM.
  • Local harness: svcomp test run passes --testcase-timeout-s as --cpu-limit.
  • Removed: the unused svcomp/SVCompHandler.py (dead code with -Xmx32g and the 60 s timeout). The remaining 60 s defaults are in non-sv-comp drivers, or are overridden with None in sv-comp mode.

Measurements

Config: --sv-comp, nosa, cgroup MemoryMax=15000000000, explorer --cpu-limit = harness limit. Base is the integration branch.

set runs result
all baseline-correct runs, 300 s, 2 cores 1047 0 flips; total CPU to verdict -28 % (median -26 %, no run slower than 1.03x); max peak RSS 459 MB
re-check of 70 of them on the final commit 70 0 flips
affected (heap-over-15g, uncaught-oom KB records; all baseline oom/no-verdict runs) + 8 long timeouts, 900 s, 4 cores 55 0 point changes. Base: 27 OOM kills, 12 timeouts, 1 stall. New: all 55 end with a verdict (54 UNKNOWN, 1 VIOLATION same as base at 900 s)
runexec (BenchExec 3.35) of the package, 15e9 B / 900 s CPU / 960 s wall 4 SpdyStream VIOLATION + witness; Conflict UNKNOWN (peak 14.3 GB); BinaryTreeSearch-MemSat01 UNKNOWN (13.3 GB); MinePump spec1-5_product11 UNKNOWN at 881 s CPU. Before the cgroup-CPU and output fixes, the last one was TIMEOUT

New wrong verdicts: 0. Net points: 0. The gain is that 40 runs which used to be OOM/TIMEOUT/stall now give a clean UNKNOWN, and there is more exploration per CPU second. On the 900 s timeout sample, rounds went up 3-28 % (TCAS 363→464, HPack 265→322). Adjustable-Inner-Count-Halving finds its VIOLATION after 289 s instead of 379 s, which would be +1 under a 300 s limit.

The quick "oom" runs (4-6 s at 190 MB): these are not target OOMs. /usr/bin/time only counted the explorer, because the killed JVM was never waited for. The executor actually ran for about 150 s: the solver picked a nondet loop bound of 2^30 (BinaryTreeSearch-*, SortedListInsert, SatFibonacci, DistinguishedNameParser, InfiniteLoop), and the trace grew until the cgroup limit, about half of it Z3 native memory. They now end UNKNOWN. A target OutOfMemoryError (new int[2^31-1] in InsertionSort/MergeSort/Tsp) is not a property violation: it is neither a RuntimeException nor an assert. These runs already ended UNKNOWN, and now they are explicitly recorded as out_of_memory.

CPU vs wall (4 cores): base 1.17-1.6 CPU-s per wall-s on long runs, C1-only 1.02-1.16. So the 900 s CPU limit is reached after 760-860 s of wall time, before the 960 s hard wall limit.

Tests

tests/test_resource_budget.py (23 cases):

  • cgroup v1/v2 limit and CPU parsing;
  • the derived limits;
  • the monitor killing the executor / interrupting a query / ending the run;
  • driver verdicts after a stopped run;
  • no query after the deadline.

The existing explorer unit tests pass.

Known limits

  • Executor runs that never finish (2^30-iteration loops) still use CPU until the deadline. A per-round budget is not part of this PR.
  • The memory stop relies on polling. The executor was seen growing by 2 GB/s, which the 2 GB headroom covers.
  • The runexec check ran without cpuset delegation (cores pinned with taskset) and on a hand-assembled package directory, not a built archive. A full BenchExec campaign was not run.
  • Pre-existing, not changed here: a ClassNotFoundException of Main gives "No symbolic vars" and therefore SAFE.

🤖 Generated with Claude Code

nils-loose and others added 10 commits October 18, 2026 23:05
It is not imported anywhere (SVCompDriver replaced it) and still carried
the old -Xmx32g heap and 60 s executor timeout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The explorer and all executor JVMs share SV-COMP's limits (15 GB, 900 s
CPU time), but the executor got -Xmx32g, its JavaSMT Z3 context grows
outside the heap (about as much as the heap), the explorer's Z3 can
allocate gigabytes in one query, and nothing watched the CPU time. Runs
ended as OUT OF MEMORY or TIMEOUT instead of with a verdict.

A monitor thread (svcomp/ResourceBudget.py) now polls the memory and CPU
time of the explorer and its descendants:
- The limits default to the competition's (--memory-limit 15000 MB,
  --cpu-limit 900 s); a lower cgroup memory limit wins, and --cpu-used
  adds what the run used before the explorer started.
- The executor heap is derived from the memory limit (5483 MiB for
  15 GB) and Z3's own memory limit (memory_max_size) is set below it.
- 2 GB below the memory limit the executor run in progress is killed
  with SIGKILL (no shutdown hook sends a trace cut short) and the
  exploration goes on with other branches; a Z3 query is interrupted.
- 20 s before the CPU limit the exploration stops with UNKNOWN; a
  violation the stopped run printed is still reported.
- If the main thread does not wind down, or the explorer alone is too
  large, the monitor reports UNKNOWN and exits.

A run cut short (stopped by the budget, or an OutOfMemoryError in the
executor) leaves its path unexplored, so it rules out SAFE like the
other soundness downgrades. Its trace is not checked for divergence.
stats.json gets a 'resources' section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every round starts a new JVM that runs for one to a few seconds. On
long runs (4 cores) the C2 compiler threads took 41-44% of the executor
JVMs' CPU time (MinePump spec1-5_product11, TCAS_prop1, Ramp-and-Hold),
work that such short runs never earn back; GC took 0.2%.
With -XX:TieredStopAtLevel=1 the same 120 s of CPU time give 22-53%
more rounds (44 -> 63, 59 -> 90, 49 -> 60). The serial GC or -Xint do
not help (-Xint: 33 rounds).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
run_swat.py passes the CPU time already spent on copying and compiling
the task (--cpu-used), since the limit covers the whole run; the memory
and CPU limits are the explorer's defaults, which are the competition's.
The local harness passes --testcase-timeout-s as the CPU limit, so the
explorer stops with UNKNOWN before the harness kills it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While a killed executor exits, or an interrupted query returns, the
next polls see the memory still over the threshold; they no longer
count and log it as another stop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Z3's memory_max_size made Z3 abort the explorer (SIGABRT, no verdict) on
float-nonlinear-calculation/Conflict, and an interrupt does not stop Z3
while it bit-blasts, during which it grew by 2 GB/s. Without the Z3
limit, a query that is still over the threshold 1 s after its interrupt,
or is within 1 GB of the memory limit, now ends the run with UNKNOWN
(Conflict: at 14.1 GB of 15 GB, before: killed at 15 GB).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under BenchExec the run's cgroup also holds run-swat.sh and run_swat.py,
which kept running next to the explorer (34 s of CPU time in 900 s on
MinePump spec1-5_product11), so the explorer's deadline came after
BenchExec's limit. Where the memory limit comes from a cgroup, the CPU
time is now at least that cgroup's (cpu.stat usage_usec).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The walk logged one INFO line per branch node in every round: 270 of
the 320 MB of explorer.log in 900 s on MinePump spec1-5_product11. In a
BenchExec container the log is kept in memory, and writing it cost CPU
time. It is now at a TRACE level below DEBUG.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
run_swat.py collected the explorer's whole output, re-logged it line by
line and then searched it for the verdict. After a 900 s run (300 MB of
output) that took more than 20 s of CPU time, after the explorer had
stopped in time, so BenchExec still ended the run as TIMEOUT; it also
held the output in memory several times. The output now goes straight
to stdout, and the verdict for printing the witness comes from the
explorer's stats.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The root logger printed several lines per branch and query to stdout:
500 MB in 900 s on MinePump spec1-5_product11, all of which run-swat.sh
writes to out.log (in memory in a BenchExec container). In sv-comp mode
the root logger is now at WARNING; explorer.log and the verdict lines
are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nils-loose added a commit that referenced this pull request Oct 19, 2026
SVCompDriver.py:
- one C1-only option (#82 and #88 both added it), in #88's
  EXECUTOR_JVM_OPTIONS, with #88's budget-derived heap;
- determine_next_step drops a stack-overflow run's violations (#78)
  before #88's out-of-memory and resource-limit handling, and reads the
  Database without copying the tree (#82);
- a run stopped by the budget is not checked for divergence; #72's
  no-trace check applies to the others;
- every query, first or retry (#74), runs under the budget's interrupt;
- #88's cut-short check joins #72's incomplete-search check, and both
  apply to every SAFE;
- the stats keep the pre-check (#73), divergence retries (#74) and
  resources (#88).
DFS.py: the per-node line is at #88's TRACE level, with the CHOICE label.
TestcaseStats.py, test_solver_rlimit.py: both sides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nils-loose added a commit that referenced this pull request Oct 19, 2026
Combining #72, #74, #78, #82 and #88: the driver tests create #88's
budget and verdict lock, and #88's tests get the stack-overflow (#78) and
copy-free (#82) Database reads and the choice flag (#76). A run reports
one verdict (#88), so the incomplete-search subtests reset the flag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nils-loose added a commit that referenced this pull request Oct 19, 2026
SVCompDriver.py: one no_trace mechanism. run_testcase decides
received_trace once (#89); a run without one is not checked for
divergence and, after the output scan, ends UNKNOWN with a single
no_trace error, which names the branch #72 used to record it for. A run
stopped by the resource budget (#88) is handled before, as in #88.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nils-loose

Copy link
Copy Markdown
Contributor Author

Included in dev through #92 (merge commit 534c182), together with the fixes for interactions between PRs. This PR stays as the record of the change and its measured flips.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant