LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
ctmc_memory_gate.h
Go to the documentation of this file.
1#ifndef LINE_API_MC_CTMC_MEMORY_GATE_H
2#define LINE_API_MC_CTMC_MEMORY_GATE_H
3
4/**
5 * @file
6 * @ingroup api_mc
7 * Host-aware memory pre-gate for SolverCTMC.
8 *
9 * Port of `matlab/src/api/mc/ctmc_memory_gate.m`, mirrored by the JAR
10 * (`MemoryGuard.gate`) and native Python (`ctmc_memory_gate`).
11 *
12 * THE POLICY IS REFUSAL, NOT A WARNING. When the predicted peak exceeds the
13 * safe budget the solve is REFUSED with an error naming the two numbers, and
14 * only an explicit `force` downgrades that to a warning. A warning the caller
15 * can walk past is the wrong contract here: the failure mode it precedes is the
16 * OOM killer taking the whole process, which produces no diagnostic at all and,
17 * observed on 2026-08-01, took down five sessions and the terminal hosting
18 * them. Refusing costs the caller a re-run with another solver; not refusing
19 * costs it everything running on the machine.
20 *
21 * MDD AND CFTP ARE NEVER GATED. Both are served by their own analyzers and
22 * return before any explicit-generator path, so the gate is not on their route.
23 * That exemption is load-bearing rather than incidental: `mdd` holds the
24 * reachable set in a decision diagram whose size is governed by the diagram's
25 * compression, not by the state count, so it routinely solves models this
26 * estimator scores at exp(200). Gating on explicit size would clamp exactly the
27 * method that exists to beat it. Keep any new non-explicit method on the same
28 * side of the gate.
29 *
30 * The predictor is the power law of the reference, `bytes = alpha * N^beta`,
31 * evaluated in log space. MATLAB and Python calibrate alpha/beta once per host
32 * by profiling a sparse LU and cache the fit; this port uses the reference's
33 * FALLBACK coefficients unconditionally, which is the same branch those two
34 * take when calibration cannot run. That is deliberate: the fallback is the
35 * conservative end of the fitted range, it is deterministic across hosts and
36 * runs, and a gate that silently changes its verdict after a background
37 * profiling step is worse than one that is slightly pessimistic.
38 */
39
40#include <cmath>
41#include <sstream>
42#include <string>
43
44#if defined(__unix__) || defined(__APPLE__)
45#include <unistd.h>
46#endif
47
48namespace line {
49namespace mc {
50
51/// 8 bytes of value plus 8 amortized for the index, per stored nonzero.
52constexpr double CTMC_BYTES_PER_NZ = 16.0;
53/// Fraction of available memory the solver may target.
54constexpr double CTMC_DEFAULT_SAFETY_FRACTION = 0.6;
55/// Fallback power-law coefficients, identical to MATLAB and Python.
56constexpr double CTMC_FALLBACK_ALPHA = CTMC_BYTES_PER_NZ * 8.0;
57constexpr double CTMC_FALLBACK_BETA = 1.3;
58/// Conservative available-memory default when the host probe fails.
59constexpr double CTMC_FALLBACK_AVAIL_BYTES = 1.0 * 1024.0 * 1024.0 * 1024.0;
60
61/** The gate verdict, plus the message the caller reports either way. */
63 bool ok = true;
64 std::string message;
65 double predicted_gb = 0.0;
66 double budget_gb = 0.0;
67};
68
69/**
70 * Available physical memory in bytes.
71 *
72 * Never throws: a probe failure returns the conservative fallback, because a
73 * gate that cannot measure the host must not thereby become permissive.
74 */
76#if defined(_SC_AVPHYS_PAGES) && defined(_SC_PAGE_SIZE)
77 const long pages = ::sysconf(_SC_AVPHYS_PAGES);
78 const long page_size = ::sysconf(_SC_PAGE_SIZE);
79 if (pages > 0 && page_size > 0)
80 return static_cast<double>(pages) * static_cast<double>(page_size);
81#endif
83}
84
85/**
86 * Decide whether a state space of log-size `log_nstates` can be solved here.
87 *
88 * @param log_nstates natural log of the worst-case state count
89 * @param force true downgrades a refusal to a warning
90 * @param safety_fraction fraction of available memory the solve may target
91 */
92inline CtmcGateResult ctmc_memory_gate(double log_nstates, bool force = false,
93 double safety_fraction = CTMC_DEFAULT_SAFETY_FRACTION) {
95 const double avail = ctmc_available_memory_bytes();
96 const double budget = safety_fraction * avail;
97
98 // Compared in LOG space: the predicted byte count of an intractable model
99 // overflows a double long before it exceeds the budget, and inf > x is a
100 // comparison that has already lost the margin it was meant to report.
101 const double log_pred = std::log(CTMC_FALLBACK_ALPHA) + CTMC_FALLBACK_BETA * log_nstates;
102 const double log_budget = std::log(budget > 1.0 ? budget : 1.0);
103
104 res.predicted_gb = std::exp(log_pred < 700.0 ? log_pred : 700.0) / (1024.0 * 1024.0 * 1024.0);
105 res.budget_gb = budget / (1024.0 * 1024.0 * 1024.0);
106 if (log_pred <= log_budget) return res;
107
108 // An intractable model predicts a byte count with hundreds of digits, and
109 // printing it in full says nothing a magnitude does not. Above a petabyte
110 // the figure is reported as a power of ten.
111 std::ostringstream pred;
112 const double log10_gb = (log_pred - std::log(1024.0 * 1024.0 * 1024.0)) / std::log(10.0);
113 if (log10_gb > 6.0) {
114 pred.setf(std::ios::fixed);
115 pred.precision(1);
116 pred << "1e" << log10_gb;
117 } else {
118 pred.setf(std::ios::fixed);
119 pred.precision(2);
120 pred << res.predicted_gb;
121 }
122
123 std::ostringstream os;
124 os.setf(std::ios::fixed);
125 os.precision(2);
126 os << "CTMC predicted peak memory ~" << pred.str() << " GB exceeds the safe budget ~"
127 << res.budget_gb << " GB (" << static_cast<int>(100.0 * safety_fraction + 0.5) << "% of "
128 << avail / (1024.0 * 1024.0 * 1024.0)
129 << " GB available). Reduce the state space (e.g. lower 'cutoff'), use the 'mdd' method, "
130 "use another solver (MVA/NC/FLD), or set force=true to override.";
131 res.message = os.str();
132 res.ok = force;
133 return res;
134}
135
136} // namespace mc
137} // namespace line
138
139#endif // LINE_API_MC_CTMC_MEMORY_GATE_H
constexpr double CTMC_DEFAULT_SAFETY_FRACTION
Fraction of available memory the solver may target.
constexpr double CTMC_BYTES_PER_NZ
8 bytes of value plus 8 amortized for the index, per stored nonzero.
constexpr double CTMC_FALLBACK_ALPHA
Fallback power-law coefficients, identical to MATLAB and Python.
double ctmc_available_memory_bytes()
Available physical memory in bytes.
constexpr double CTMC_FALLBACK_AVAIL_BYTES
Conservative available-memory default when the host probe fails.
CtmcGateResult ctmc_memory_gate(double log_nstates, bool force=false, double safety_fraction=CTMC_DEFAULT_SAFETY_FRACTION)
Decide whether a state space of log-size log_nstates can be solved here.
constexpr double CTMC_FALLBACK_BETA
The gate verdict, plus the message the caller reports either way.