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 const auto admitting_all = [&](std::size_t k) {
209 std::size_t n = 0;
210 for (std::size_t i = 0; i < M; ++i)
211 if (!disabled_at(i, k)) ++n;
212 return n < 1 ? std::size_t(1) : n;
213 };
214 const auto admitting_rem = [&](std::size_t k) {
215 std::size_t n = 0;
216 for (std::size_t i = 0; i < M; ++i) {
217 const SchedStrategy sc = sn.stations[i].sched;
218 if (!(sc == SchedStrategy::EXT || is_share(sc))) continue;
219 if (!disabled_at(i, k)) ++n;
220 }
221 return n;
222 };
223 if (n_ord == 0) {
224 for (std::size_t k = 0; k < K; ++k)
225 log_nstates += place(nk_eff[k], static_cast<double>(admitting_all(k)));
226 } else {
227 const std::size_t m_rem = M - n_ord;
228 for (std::size_t k = 0; k < K; ++k)
229 if (!is_buffered[k])
230 log_nstates += place(nk_eff[k], static_cast<double>(admitting_all(k)));
231 std::vector<int> caps;
232 double grid = 1.0;
233 for (std::size_t k = 0; k < K; ++k)
234 if (is_buffered[k]) {
235 caps.push_back(static_cast<int>(std::floor(nk_eff[k])));
236 grid *= (std::floor(nk_eff[k]) + 1.0);
237 }
238 if (grid <= kOrderGridMax) {
239 std::vector<std::vector<int>> caps_per;
240 std::vector<double> cap_tot;
241 std::vector<int> njb;
242 for (std::size_t k = 0; k < K; ++k)
243 if (is_buffered[k]) njb.push_back(static_cast<int>(std::floor(nk_eff[k])));
244 for (std::size_t i = 0; i < M; ++i) {
245 const SchedStrategy sc = sn.stations[i].sched;
246 if (sc == SchedStrategy::EXT || is_share(sc)) continue;
247 std::vector<int> per;
248 std::size_t bi = 0;
249 for (std::size_t k = 0; k < K; ++k)
250 if (is_buffered[k]) {
251 int c = njb[bi];
252 if (i < sn.classcap.size() && k < sn.classcap[i].size() &&
253 std::isfinite(sn.classcap[i][k]))
254 c = std::min(c, static_cast<int>(std::floor(sn.classcap[i][k])));
255 per.push_back(c);
256 ++bi;
257 }
258 caps_per.push_back(per);
259 const double c = sn.stations[i].cap;
260 cap_tot.push_back(std::isfinite(c) && c >= 0
261 ? std::floor(c) : std::numeric_limits<double>::infinity());
262 }
263 log_nstates += log_ordered_joint(caps_per, cap_tot, njb, m_rem);
264 } else {
265 double total = 0.0;
266 for (int c : caps) total += c;
267 const double lkb = std::log(static_cast<double>(Kb));
268 log_nstates += static_cast<double>(n_ord) *
269 ((total + 1.0) * lkb - std::log(static_cast<double>(Kb) - 1.0) +
270 std::log1p(-std::exp(-(total + 1.0) * lkb)));
271 for (std::size_t k = 0; k < K; ++k)
272 if (is_buffered[k]) {
273 const std::size_t mk = admitting_rem(k);
274 if (mk >= 1) log_nstates += place(nk_eff[k], static_cast<double>(mk));
275 }
276 }
277 }
278
279 // A sharing discipline can hold every job in service at once; a queueing one
280 // holds at most its server count.
281 for (std::size_t i = 1; i <= M; ++i) {
282 const SchedStrategy sched = sn.stations[i - 1].sched;
283 const bool share = sched == SchedStrategy::INF || sched == SchedStrategy::PS ||
284 sched == SchedStrategy::DPS || sched == SchedStrategy::GPS ||
285 sched == SchedStrategy::PSPRIO || sched == SchedStrategy::DPSPRIO ||
286 sched == SchedStrategy::GPSPRIO || sched == SchedStrategy::LPS;
287 for (std::size_t r = 1; r <= K; ++r) {
288 const double p = static_cast<double>(sn.phasessz_of(i, r));
289 if (!std::isfinite(p) || p <= 1.0) continue;
290 double m;
291 if (sched == SchedStrategy::EXT)
292 m = 1.0;
293 else if (share)
294 m = nk_eff[r - 1];
295 else
296 m = std::min(nk_eff[r - 1], sn.stations[i - 1].nservers);
297 if (!std::isfinite(m)) m = nk_eff[r - 1];
298 log_nstates += std::lgamma(1.0 + m + p - 1.0) - std::lgamma(1.0 + p - 1.0) -
299 std::lgamma(1.0 + m);
300 }
301 }
302
303 // Round-robin routing is stateful: the pointer over the outgoing links is
304 // part of the state. The reference reads `sn.connmatrix`; this port has no
305 // such field and takes the out-degree from `rtnodes`, the same graph after
306 // the refresh resolved the routing strategies.
307 for (std::size_t ind = 1; ind <= sn.nodes.size(); ++ind) {
308 const std::vector<RoutingStrategy>& rt_i = sn.nodes[ind - 1].routing;
309 std::size_t nrr = 0;
310 for (std::size_t r = 0; r < K && r < rt_i.size(); ++r)
311 if (rt_i[r] == RoutingStrategy::RROBIN || rt_i[r] == RoutingStrategy::WRROBIN) ++nrr;
312 if (nrr == 0) continue;
313 const std::size_t nout = sn.downstream_stations(ind).size();
314 if (nout <= 1) continue;
315 log_nstates += static_cast<double>(nrr) * std::log(static_cast<double>(nout));
316 }
317
318 return log_nstates;
319}
320
321} // namespace mc
322} // namespace line
323
324#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:389
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.
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