LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
ctmc_state_space_logsize.h
Go to the documentation of this file.
1#ifndef LINE_API_MC_CTMC_STATE_SPACE_LOGSIZE_H
2#define LINE_API_MC_CTMC_STATE_SPACE_LOGSIZE_H
3
4/**
5 * @file
6 * @ingroup api_mc
7 * Worst-case log-size of the CTMC state space induced by a NetworkStruct.
8 *
9 * Port of `matlab/src/api/mc/ctmc_state_space_logsize.m`. The estimate is the
10 * product of four factors, summed in log space:
11 *
12 * 1. job placements: stars-and-bars C(n_k+M-1, M-1) per class over the
13 * stations that keep no ordered buffer, with an open class truncated at
14 * the cutoff;
15 * 2. buffer orderings: a station outside the share family keeps the CLASS
16 * SEQUENCE of the jobs it holds, so with K>1 classes one occupancy vector
17 * is as many states as its sequences;
18 * 3. service phases: the phase count raised to the number of jobs that can be
19 * in service concurrently at the station;
20 * 4. routing state: one pointer over the outgoing links per (node, class)
21 * routed RROBIN or WRROBIN.
22 *
23 * It is computed in LOG space throughout because the quantity it exists to
24 * detect overflows a double: the `intractableCTMC` fixture (8 PS stations,
25 * N=400, Erlang-5) sits at exp(200), and a linear-space estimator would report
26 * `inf` for everything above exp(709) and lose the ability to rank one
27 * intractable model against another.
28 *
29 * This is the quantity fed to `ctmc_memory_gate`. It is separate from the gate
30 * so a caller such as SolverAUTO can screen CTMC out of a ranking without
31 * building the chain.
32 */
33
34#include <algorithm>
35#include <cmath>
36#include <cstddef>
37#include <string>
38#include <limits>
39#include <vector>
40
43
44namespace line {
45namespace mc {
46
47
48
49/** Largest (m_1..m_K) box the exact ordered-buffer DP will walk. */
50inline constexpr double kOrderGridMax = 1.0e6;
51
52/**
53 * Log count of (placement, ordering) configurations over ALL order-preserving
54 * stations at once, POPULATION CONSERVED. `caps_per[a][k]` bounds class k at
55 * ordered station a, `cap_tot[a]` bounds the buffer TOTAL there (a finite
56 * station capacity is a slot count, not a per-class bound), `njobs` is the
57 * population to share out, and `m_rem` share stations take the leftovers.
58 *
59 * Cutoff truncates an OPEN class's population in the network exactly as the
60 * plain stars-and-bars term treats it, so open classes are conserved too.
61 */
62inline double log_ordered_joint(const std::vector<std::vector<int>>& caps_per,
63 const std::vector<double>& cap_tot,
64 const std::vector<int>& njobs, std::size_t m_rem) {
65 const std::size_t Kb = njobs.size();
66 std::vector<std::size_t> dims(Kb);
67 std::size_t nstate = 1;
68 for (std::size_t k = 0; k < Kb; ++k) { dims[k] = njobs[k] + 1; nstate *= dims[k]; }
69 const double NEG = -std::numeric_limits<double>::infinity();
70 auto idx_of = [&](const std::vector<int>& v) {
71 std::size_t ix = 0, mult = 1;
72 for (std::size_t k = 0; k < Kb; ++k) { ix += v[k] * mult; mult *= dims[k]; }
73 return ix;
74 };
75 auto sub_of = [&](std::size_t ix) {
76 std::vector<int> v(Kb);
77 for (std::size_t k = 0; k < Kb; ++k) { v[k] = static_cast<int>(ix % dims[k]); ix /= dims[k]; }
78 return v;
79 };
80 std::vector<double> L(nstate, NEG);
81 L[idx_of(njobs)] = 0.0;
82 for (std::size_t a = 0; a < caps_per.size(); ++a) {
83 std::vector<double> Ln(nstate, NEG);
84 for (std::size_t si = 0; si < nstate; ++si) {
85 if (!std::isfinite(L[si])) continue;
86 const std::vector<int> rem = sub_of(si);
87 std::vector<int> av(Kb);
88 for (std::size_t k = 0; k < Kb; ++k) av[k] = std::min(caps_per[a][k], rem[k]);
89 std::vector<int> m(Kb, 0);
90 for (;;) {
91 int t = 0;
92 for (std::size_t k = 0; k < Kb; ++k) t += m[k];
93 if (!(std::isfinite(cap_tot[a]) && t > cap_tot[a])) {
94 double v = L[si] + std::lgamma(static_cast<double>(t) + 1.0);
95 for (std::size_t k = 0; k < Kb; ++k) v -= std::lgamma(static_cast<double>(m[k]) + 1.0);
96 std::vector<int> nx(Kb);
97 for (std::size_t k = 0; k < Kb; ++k) nx[k] = rem[k] - m[k];
98 const std::size_t di = idx_of(nx);
99 if (!std::isfinite(Ln[di])) Ln[di] = v;
100 else { const double mx = std::max(Ln[di], v);
101 Ln[di] = mx + std::log(std::exp(Ln[di] - mx) + std::exp(v - mx)); }
102 }
103 std::size_t pos = 0;
104 while (pos < Kb && m[pos] == av[pos]) { m[pos] = 0; ++pos; }
105 if (pos == Kb) break;
106 ++m[pos];
107 }
108 }
109 L.swap(Ln);
110 }
111 std::vector<double> terms;
112 for (std::size_t si = 0; si < nstate; ++si) {
113 if (!std::isfinite(L[si])) continue;
114 const std::vector<int> rem = sub_of(si);
115 double v = L[si];
116 if (m_rem >= 1) {
117 for (std::size_t k = 0; k < Kb; ++k) {
118 const double r = rem[k];
119 v += std::lgamma(r + static_cast<double>(m_rem)) - std::lgamma(r + 1.0) -
120 std::lgamma(static_cast<double>(m_rem));
121 }
122 } else {
123 bool leftover = false;
124 for (std::size_t k = 0; k < Kb; ++k) if (rem[k] > 0) leftover = true;
125 if (leftover) continue;
126 }
127 terms.push_back(v);
128 }
129 if (terms.empty()) return NEG;
130 double top = terms[0];
131 for (double v : terms) top = std::max(top, v);
132 double acc = 0.0;
133 for (double v : terms) acc += std::exp(v - top);
134 return top + std::log(acc);
135}
136
137/** Options the estimator reads; only the cutoff matters. */
139 double cutoff = -1.0; ///< < 0 or non-finite = not given, take the solver default
140};
141
142/**
143 * Worst-case log state-space size of `sn`.
144 *
145 * @param sn the struct, ideally after `sn_nonmarkov_toph`, since a
146 * non-Markovian service becomes phases the raw struct does not carry
147 * @return natural log of the worst-case state count
148 */
149template <class T>
154
155 const std::size_t M = sn.nstations;
156 const std::size_t K = sn.nclasses;
157 if (M == 0 || K == 0) return 0.0;
158
159 // The analyzer's own default for an open or mixed model, so the estimate is
160 // taken against the space that would actually be built.
161 double cutoff = opt.cutoff;
162 if (!(cutoff > 0.0) || !std::isfinite(cutoff))
163 cutoff = std::ceil(std::pow(6000.0, 1.0 / static_cast<double>(M * K)));
164
165 // ORDERED BUFFERS, computed EXACTLY -- see the python twin for the measured
166 // numbers. Omitting it under-priced gallery_mmap1_multiclass 311x; BOUNDING
167 // it instead of computing it double counts and refused a working model.
168 const auto is_share = [](SchedStrategy s) {
169 return s == SchedStrategy::INF || s == SchedStrategy::PS || s == SchedStrategy::DPS ||
170 s == SchedStrategy::GPS || s == SchedStrategy::PSPRIO ||
171 s == SchedStrategy::DPSPRIO || s == SchedStrategy::GPSPRIO ||
172 s == SchedStrategy::LPS;
173 };
174 std::vector<bool> is_buffered(K, true);
175 std::size_t Kb = 0;
176 for (std::size_t k = 0; k < K; ++k) {
177 if (k < sn.issignal.size() && sn.issignal[k]) is_buffered[k] = false;
178 if (is_buffered[k]) ++Kb;
179 }
180 std::size_t n_ord = 0;
181 if (Kb > 1) {
182 for (std::size_t i = 0; i < M; ++i) {
183 const SchedStrategy s = sn.stations[i].sched;
184 if (s == SchedStrategy::EXT || is_share(s)) continue;
185 ++n_ord;
186 }
187 }
188
189 double log_nstates = 0.0;
190 std::vector<double> nk_eff(K, 0.0);
191 const std::vector<double>& njobs = sn.njobs();
192 for (std::size_t k = 0; k < K; ++k)
193 nk_eff[k] = (k < njobs.size() && std::isfinite(njobs[k])) ? njobs[k] : cutoff;
194
195 const auto place = [&](double nk, double ms) {
196 return std::lgamma(1.0 + nk + ms - 1.0) - std::lgamma(1.0 + ms - 1.0) -
197 std::lgamma(1.0 + nk);
198 };
199 // A ZERO per-class capacity means the class is DISABLED at that station, so it
200 // never occupies a slot and the placement term must spread it over the
201 // stations that admit it, not over all M. ld_whittle_bandwidth disables each
202 // of its three PS routes for the other two classes; counting all M=4 priced it
203 // at C(9,6)^3 = 592704 states, 7852 GB under the quadratic byte model, and the
204 // gate refused a model whose true space is 7^3 = 343 and solves at once.
205 const auto disabled_at = [&](std::size_t i, std::size_t k) {
206 return i < sn.classcap.size() && k < sn.classcap[i].size() && sn.classcap[i][k] == 0.0;
207 };
208 // ZERO VISITS IS THE SECOND WAY IN, and it is the state space generator's own
209 // rule: a (station, class) pair whose chain visit ratio is zero gets capacity
210 // 0 there, because a class the routing never delivers to a station cannot be
211 // there. Only an SPN PLACE escapes, keeping its declared initial marking, so a
212 // Place is left alone here too. Reading only classcap missed all of it, since
213 // that zeroes a pair whose SERVICE RATE is NaN -- the class is disabled at the
214 // station, not unrouted to it. A class that exists only as a switch target on
215 // some departure arc has a good service rate everywhere and a visit ratio of
216 // zero everywhere: a Source -> FCFS Queue -> Sink model with such a class was
217 // priced at 184755 states against the 11 the generator enumerates, predicted
218 // 17.62 GB, and had the gate refuse a model that solves in 0.05 s.
219 // Visit rows are STATEFUL-node indexed, not station indexed. A pair the chain
220 // never enters carries floating-point noise (~1e-17) rather than an exact 0,
221 // which is the tolerance below, shared with the generator.
222 const double kVisitZeroTol = 1.0e-12;
223 std::vector<std::vector<bool>> never(M, std::vector<bool>(K, false));
224 if (!sn.visits.empty() && !sn.chains.empty()) {
225 for (std::size_t i = 0; i < M; ++i) {
226 if (sn.stations[i].nodetype == lang::NodeType::Place) continue;
227 const std::size_t nd = sn.node_of_station(i + 1);
228 if (nd == 0) continue;
229 const std::size_t isf1 = sn.stateful_index(nd);
230 if (isf1 == 0) continue;
231 const std::size_t isf = isf1 - 1;
232 for (std::size_t k = 0; k < K; ++k) {
233 std::size_t c = sn.nchains;
234 for (std::size_t ci = 0; ci < sn.chains.size(); ++ci)
235 if (k < sn.chains[ci].size() && sn.chains[ci][k]) { c = ci; break; }
236 if (c >= sn.visits.size()) continue;
237 const auto& V = sn.visits[c];
238 if (isf >= V.rows() || k >= V.cols()) continue;
239 const double v = num_traits<T>::to_double(V(isf, k));
240 if (std::isfinite(v) && std::fabs(v) < kVisitZeroTol) never[i][k] = true;
241 }
242 }
243 }
244 const auto admits = [&](std::size_t i, std::size_t k) {
245 return !disabled_at(i, k) && !never[i][k];
246 };
247 const auto admitting_count = [&](std::size_t k) {
248 std::size_t n = 0;
249 for (std::size_t i = 0; i < M; ++i)
250 if (admits(i, k)) ++n;
251 return n;
252 };
253 const auto admitting_all = [&](std::size_t k) {
254 const std::size_t n = admitting_count(k);
255 return n < 1 ? std::size_t(1) : n;
256 };
257 const auto admitting_rem = [&](std::size_t k) {
258 std::size_t n = 0;
259 for (std::size_t i = 0; i < M; ++i) {
260 const SchedStrategy sc = sn.stations[i].sched;
261 if (!(sc == SchedStrategy::EXT || is_share(sc))) continue;
262 if (admits(i, k)) ++n;
263 }
264 return n;
265 };
266 // A class no station admits holds no job anywhere, so its population is zero
267 // rather than the cutoff; leaving it at the cutoff made the class occupy
268 // ordered buffers it can never enter.
269 for (std::size_t k = 0; k < K; ++k)
270 if (nk_eff[k] > 0.0 && admitting_count(k) == 0) nk_eff[k] = 0.0;
271 if (n_ord == 0) {
272 for (std::size_t k = 0; k < K; ++k)
273 log_nstates += place(nk_eff[k], static_cast<double>(admitting_all(k)));
274 } else {
275 const std::size_t m_rem = M - n_ord;
276 for (std::size_t k = 0; k < K; ++k)
277 if (!is_buffered[k])
278 log_nstates += place(nk_eff[k], static_cast<double>(admitting_all(k)));
279 std::vector<int> caps;
280 double grid = 1.0;
281 for (std::size_t k = 0; k < K; ++k)
282 if (is_buffered[k]) {
283 caps.push_back(static_cast<int>(std::floor(nk_eff[k])));
284 grid *= (std::floor(nk_eff[k]) + 1.0);
285 }
286 if (grid <= kOrderGridMax) {
287 std::vector<std::vector<int>> caps_per;
288 std::vector<double> cap_tot;
289 std::vector<int> njb;
290 for (std::size_t k = 0; k < K; ++k)
291 if (is_buffered[k]) njb.push_back(static_cast<int>(std::floor(nk_eff[k])));
292 for (std::size_t i = 0; i < M; ++i) {
293 const SchedStrategy sc = sn.stations[i].sched;
294 if (sc == SchedStrategy::EXT || is_share(sc)) continue;
295 std::vector<int> per;
296 std::size_t bi = 0;
297 for (std::size_t k = 0; k < K; ++k)
298 if (is_buffered[k]) {
299 int c = njb[bi];
300 if (i < sn.classcap.size() && k < sn.classcap[i].size() &&
301 std::isfinite(sn.classcap[i][k]))
302 c = std::min(c, static_cast<int>(std::floor(sn.classcap[i][k])));
303 if (!admits(i, k)) c = 0;
304 per.push_back(c);
305 ++bi;
306 }
307 caps_per.push_back(per);
308 const double c = sn.stations[i].cap;
309 cap_tot.push_back(std::isfinite(c) && c >= 0
310 ? std::floor(c) : std::numeric_limits<double>::infinity());
311 }
312 log_nstates += log_ordered_joint(caps_per, cap_tot, njb, m_rem);
313 } else {
314 double total = 0.0;
315 for (int c : caps) total += c;
316 const double lkb = std::log(static_cast<double>(Kb));
317 log_nstates += static_cast<double>(n_ord) *
318 ((total + 1.0) * lkb - std::log(static_cast<double>(Kb) - 1.0) +
319 std::log1p(-std::exp(-(total + 1.0) * lkb)));
320 for (std::size_t k = 0; k < K; ++k)
321 if (is_buffered[k]) {
322 const std::size_t mk = admitting_rem(k);
323 if (mk >= 1) log_nstates += place(nk_eff[k], static_cast<double>(mk));
324 }
325 }
326 }
327
328 // A sharing discipline can hold every job in service at once; a queueing one
329 // holds at most its server count.
330 for (std::size_t i = 1; i <= M; ++i) {
331 const SchedStrategy sched = sn.stations[i - 1].sched;
332 const bool share = sched == SchedStrategy::INF || sched == SchedStrategy::PS ||
333 sched == SchedStrategy::DPS || sched == SchedStrategy::GPS ||
334 sched == SchedStrategy::PSPRIO || sched == SchedStrategy::DPSPRIO ||
335 sched == SchedStrategy::GPSPRIO || sched == SchedStrategy::LPS;
336 for (std::size_t r = 1; r <= K; ++r) {
337 const double p = static_cast<double>(sn.phasessz_of(i, r));
338 if (!std::isfinite(p) || p <= 1.0) continue;
339 double m;
340 if (sched == SchedStrategy::EXT)
341 m = 1.0;
342 else if (share)
343 m = nk_eff[r - 1];
344 else
345 m = std::min(nk_eff[r - 1], sn.stations[i - 1].nservers);
346 if (!std::isfinite(m)) m = nk_eff[r - 1];
347 log_nstates += std::lgamma(1.0 + m + p - 1.0) - std::lgamma(1.0 + p - 1.0) -
348 std::lgamma(1.0 + m);
349 }
350 }
351
352 // Round-robin routing is stateful: the pointer over the outgoing links is
353 // part of the state. The reference reads `sn.connmatrix`; this port has no
354 // such field and takes the out-degree from `rtnodes`, the same graph after
355 // the refresh resolved the routing strategies.
356 for (std::size_t ind = 1; ind <= sn.nodes.size(); ++ind) {
357 const std::vector<RoutingStrategy>& rt_i = sn.nodes[ind - 1].routing;
358 std::size_t nrr = 0;
359 for (std::size_t r = 0; r < K && r < rt_i.size(); ++r)
360 if (rt_i[r] == RoutingStrategy::RROBIN || rt_i[r] == RoutingStrategy::WRROBIN) ++nrr;
361 if (nrr == 0) continue;
362 const std::size_t nout = sn.downstream_stations(ind).size();
363 if (nout <= 1) continue;
364 log_nstates += static_cast<double>(nrr) * std::log(static_cast<double>(nout));
365 }
366
367 return log_nstates;
368}
369
370} // namespace mc
371} // namespace line
372
373#endif // LINE_API_MC_CTMC_STATE_SPACE_LOGSIZE_H
A network plus its refreshed NetworkStruct.
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
RoutingStrategy
Routing strategies, with the values of MATLAB RoutingStrategy.
Definition lang_types.h:391
double log_ordered_joint(const std::vector< std::vector< int > > &caps_per, const std::vector< double > &cap_tot, const std::vector< int > &njobs, std::size_t m_rem)
Log count of (placement, ordering) configurations over ALL order-preserving stations at once,...
double ctmc_state_space_logsize(const qn::NetworkStruct< T > &sn, const CtmcSizeOptions &opt=CtmcSizeOptions())
Worst-case log state-space size of sn.
constexpr double kOrderGridMax
Largest (m_1..m_K) box the exact ordered-buffer DP will walk.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
A queueing network and its refreshed NetworkStruct.
Options the estimator reads; only the cutoff matters.
double cutoff
< 0 or non-finite = not given, take the solver default