LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
lqn_helpers.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_SOLVERS_LN_LQN_HELPERS_H
6#define LINE_SOLVERS_LN_LQN_HELPERS_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Standalone LQN routines that SolverLN needs but does not contain.
12 *
13 * Port of matlab/src/solvers/LN/lqn_fwd_rendezvous.m and
14 * matlab/src/solvers/LN/lqn_overtake_markov.m.
15 *
16 * lqn_act_thinktime.m is DELIBERATELY ABSENT. Its whole content -- add the
17 * activity think time to the activity's service and residence time, skipping
18 * the unset (NaN in MATLAB, `disabled` here) case -- is already inline in the
19 * servt_map loop of solver_ln.h. A second copy of a two-line rule that two
20 * files would have to keep agreeing on is worse than no helper at all.
21 *
22 * BOTH ARE WIRED IN. SolverLN::construct calls lqn_fwd_rendezvous before it
23 * reads anything else out of the struct, as @@SolverLN/SolverLN.m:162-164 does,
24 * and lqn_overtake_markov is reached from update_metrics through the input
25 * mapping of lqn_analyzers.h's lqn_overtake_prob_markov. Neither construct is
26 * refused by name any longer.
27 */
28
29#include <cctype>
30#include <cmath>
31#include <cstddef>
32#include <string>
33#include <vector>
34
36#include "line/num/number.h"
37#include "line/util/error.h"
38#include "line/util/matrix.h"
39
40namespace line {
41namespace ln {
42
43using lang::CallType;
44using lqn::LqnStruct;
45
46// ---------------------------------------------------------------------------
47// lqn_fwd_rendezvous
48// ---------------------------------------------------------------------------
49
50/**
51 * Replace every forwarding chain reachable from a synchronous call by
52 * caller-side pseudo rendezvous calls to the forwarding targets.
53 *
54 * Port of LQNS Phase::addForwardingRendezvous (phase.cc). A forwarded call
55 * blocks the original caller until the LAST task in the chain replies, so the
56 * caller's blocking time spans the chain, not just the entry it named. Rather
57 * than teach every downstream stage what a FWD arc means, the chain is
58 * flattened here into ordinary SYNC arcs from the original calling activity to
59 * each entry on the chain, each with mean equal to the original call mean times
60 * the product of the forwarding probabilities on the path to it. Layer
61 * construction, think times, populations and the interlock analysis then see
62 * plain rendezvous arcs and need no forwarding case at all. This is also what
63 * LQNS does: interlock.cc drops FWD arcs outright ("Drop forward -- keep rnv")
64 * and accounts for forwarding only through these pseudo arcs.
65 *
66 * The FWD calls survive in the struct but must not contribute blocking anywhere
67 * after this point, or the chain is charged twice.
68 *
69 * Asynchronous calls into a forwarding chain are left alone: LQNS breaks the
70 * backward search at a send-no-reply, since nobody is blocked waiting for it.
71 *
72 * The MATLAB version also rebuilds the Geometric process descriptor of each
73 * rewritten call. There is nothing to port: LqnStruct carries only
74 * callproc_mean, which is the only field SolverLN reads.
75 */
76template <class T>
78 const T zero = num_traits<T>::from_int(0);
79 const T one = num_traits<T>::from_int(1);
80
81 bool any_fwd = false;
82 for (std::size_t c = 1; c <= lqn.ncalls; ++c)
83 if (lqn.calltype[c] == CallType::FWD) any_fwd = true;
84 if (!any_fwd) return;
85
86 // Frozen: the pseudo calls appended below are themselves SYNC, and
87 // rewriting them again would raise the chain to a power of its own
88 // probabilities.
89 const std::size_t ncalls0 = lqn.ncalls;
90
91 for (std::size_t cidx = 1; cidx <= ncalls0; ++cidx) {
92 if (lqn.calltype[cidx] != CallType::SYNC) continue;
93 const std::size_t aidx = lqn.callpair_src[cidx];
94 const std::size_t tidx = lqn.parent[aidx];
95 const T base_mean = lqn.callproc_mean[cidx];
96 if (base_mean <= zero) continue;
97
98 // breadth-first walk of the forwarding chain out of the sync target,
99 // carrying the probability of the path that reached each entry
100 std::vector<std::size_t> frontier{lqn.callpair_dst[cidx]};
101 std::vector<T> probs{one};
102 std::vector<std::size_t> visited;
103 auto seen = [](const std::vector<std::size_t>& v, std::size_t x) {
104 for (std::size_t e : v)
105 if (e == x) return true;
106 return false;
107 };
108
109 while (!frontier.empty()) {
110 const std::size_t eidx = frontier.front();
111 const T p_path = probs.front();
112 frontier.erase(frontier.begin());
113 probs.erase(probs.begin());
114 if (seen(visited, eidx)) continue;
115 visited.push_back(eidx);
116
117 for (std::size_t fcidx = 1; fcidx <= ncalls0; ++fcidx) {
118 if (lqn.calltype[fcidx] != CallType::FWD) continue;
119 if (lqn.callpair_src[fcidx] != eidx) continue;
120 const T fprob = lqn.callproc_mean[fcidx];
121 const std::size_t tgt = lqn.callpair_dst[fcidx];
122 const T pseudo_mean = T(base_mean * p_path * fprob);
123
124 // A chain that comes back to the caller's own task carries no
125 // pseudo arc: the caller would appear as its own client.
126 if (pseudo_mean > zero && lqn.parent[tgt] != tidx) {
127 // see _kb/06-solver-catalog.md (LN section) for rationale
128 std::size_t mrow = 0;
129 for (std::size_t scan = 1; scan <= lqn.ncalls; ++scan)
130 if (lqn.calltype[scan] == CallType::SYNC &&
131 lqn.callpair_src[scan] == aidx && lqn.callpair_dst[scan] == tgt) {
132 mrow = scan;
133 break;
134 }
135 if (mrow > 0) {
136 // an arc already exists, so the forwarded work is extra
137 // visits on it rather than a second parallel class
138 lqn.callproc_mean[mrow] = T(lqn.callproc_mean[mrow] + pseudo_mean);
139 } else {
140 const std::size_t ncall = lqn.ncalls + 1;
141 lqn.ncalls = ncall;
142 const std::size_t target_tidx = lqn.parent[tgt];
143 lqn.calltype.push_back(CallType::SYNC);
144 lqn.callpair_src.push_back(aidx);
145 lqn.callpair_dst.push_back(tgt);
146 lqn.callproc_mean.push_back(pseudo_mean);
147 lqn.callnames.push_back(lqn.names[aidx] + "=>" + lqn.names[tgt]);
148 lqn.callhashnames.push_back(lqn.hashnames[aidx] + "=>" +
149 lqn.hashnames[tgt]);
150 lqn.callsof[aidx].push_back(ncall);
151 lqn.iscaller.set(tidx, target_tidx);
152 lqn.iscaller.set(aidx, target_tidx);
153 lqn.iscaller.set(tidx, tgt);
154 lqn.iscaller.set(aidx, tgt);
155 lqn.issynccaller.set(tidx, target_tidx);
156 lqn.issynccaller.set(aidx, target_tidx);
157 lqn.issynccaller.set(tidx, tgt);
158 lqn.issynccaller.set(aidx, tgt);
159 lqn.graph.set(aidx, tgt, one);
160 lqn.taskgraph.set(tidx, target_tidx, one);
161 }
162 }
163
164 if (!seen(visited, tgt) && !seen(frontier, tgt)) {
165 frontier.push_back(tgt);
166 probs.push_back(T(p_path * fprob));
167 }
168 }
169 }
170 }
171}
172
173// ---------------------------------------------------------------------------
174// lqn_overtake_markov
175// ---------------------------------------------------------------------------
176
177namespace detail {
178
179/** True when x is a finite value of T; only a floating T can fail this. */
180template <class T>
181bool ln_finite(const T& x) {
182 return std::isfinite(num_traits<T>::to_double(x));
183}
184
185/**
186 * Transition rates of one client phase in the LQNS slice chain (slice.cc
187 * setRates). The four outputs are the probabilities of, respectively, staying
188 * in the client phase (a), completing it and revisiting the server (b),
189 * catching the server in the tested phase (c), and being held at another server
190 * first (d).
191 */
192template <class T>
193void ln_set_rates(const T& xj, const T& prA, const T& nSlices, const T& service, const T& y_ij,
194 const T& y_ik, const T& t_k, T& a, T& b, T& c, T& d) {
195 const T zero = num_traits<T>::from_int(0);
196 const T one = num_traits<T>::from_int(1);
197
198 const T y_sum = T(y_ij + y_ik + one);
199 const T slice = nSlices != zero ? T(service / nSlices) : zero;
200
201 T q0, q1, q3, q5;
202 T temp = T(xj + t_k);
203 if (!ln_finite(temp)) {
204 q0 = one;
205 q3 = zero;
206 } else if (temp != zero) {
207 q0 = T(xj / temp);
208 q3 = T(t_k / temp);
209 } else {
210 q0 = zero;
211 q3 = one;
212 }
213 temp = T(xj + slice);
214 if (!ln_finite(temp)) {
215 q1 = zero;
216 q5 = one;
217 } else if (temp != zero) {
218 q1 = T(xj / temp);
219 q5 = T(slice / temp);
220 } else {
221 q1 = one;
222 q5 = zero;
223 }
224 const T q2 = T(y_ik / y_sum), q4 = T(y_ij / y_sum), q6 = T(prA / y_sum);
225
226 a = T(q5 + q1 * q2 * q3);
227 b = T(q1 * q6);
228 c = T(q1 * q4);
229 d = T(q0 * q1 * q2);
230}
231
232} // namespace detail
233
234/**
235 * Overtaking probability from the LQNS phased-server Markov chain.
236 *
237 * Layer-1 port of LQNS V6 (slice.cc setRates and prOvertakingStates,
238 * overtake.cc computeOvertaking) for the single-conditioning case, where the
239 * calling entry and the conditioning entry coincide. Overtaking is the event
240 * that a client's next request reaches the server while the server is still
241 * running the second phase of the PREVIOUS request from the same client: the
242 * early reply released the client, so two of its requests can be in flight at
243 * once and the later one can pass the earlier one. `lqns -t overtaking` prints
244 * the same quantity; on its 31-overtaking model, phase 2 of the server gives
245 * 0.5.
246 *
247 * The chain is over client phases 0..maxPhaseA, phase 0 being the client's
248 * think slice. Row p of `clientPhases` is
249 * [nSlices, service, y_ij, y_ik, t_k]
250 * with nSlices = 1 + the rendezvous calls made in phase p (the slice count the
251 * phase is chopped into), service = total host residence of the phase, y_ij =
252 * calls to the tested server's task, y_ik = calls to any other task, and t_k =
253 * mean time spent at those other tasks. Row 0's service is the think time.
254 * `xj` is the server's residence time in the phase being tested, `prVisit` the
255 * client entry's visit probability, and y_aj[0] the client's total calls to the
256 * server with y_aj[i] the calls made in client phase i.
257 *
258 * Every rate is a ratio of times (xj/(xj+slice) and so on), so the result is
259 * invariant to a common rescaling of all the time inputs, as a probability must
260 * be.
261 *
262 * Reference: Franks and Woodside, "Effectiveness of early replies in
263 * client-server systems", Perf. Eval. 36 (1999).
264 */
265template <class T>
266T lqn_overtake_markov(const Matrix<T>& clientPhases, const T& prVisit, const T& xj,
267 const std::vector<T>& y_aj) {
268 const T zero = num_traits<T>::from_int(0);
269 const T one = num_traits<T>::from_int(1);
270
271 if (clientPhases.rows() < 1 || clientPhases.cols() < 5)
272 throw InputError(
273 "lqn_overtake_markov: clientPhases must have five columns "
274 "[nSlices service y_ij y_ik t_k] and one row per client phase 0..maxPhaseA");
275 const std::size_t nStates = clientPhases.rows();
276 const std::size_t maxPhaseA = nStates - 1;
277 if (y_aj.size() < nStates)
278 throw InputError(
279 "lqn_overtake_markov: y_aj must hold the total call count and one entry per client "
280 "phase");
281
282 // setRates for each client phase. prVisit applies only to the last phase,
283 // which is the one that ends the client cycle and issues the next request.
284 std::vector<T> a(nStates, zero), b(nStates, zero), c(nStates, zero), d(nStates, zero);
285 for (std::size_t p = 0; p < nStates; ++p) {
286 const T prA = (p == maxPhaseA) ? prVisit : one;
287 detail::ln_set_rates(xj, prA, clientPhases(p, 0), clientPhases(p, 1), clientPhases(p, 2),
288 clientPhases(p, 3), clientPhases(p, 4), a[p], b[p], c[p], d[p]);
289 }
290
291 // prOvertakingStates. prod_of_b folds the self-loop at a phase (probability
292 // d of a detour to another server) into the phase's forward probability;
293 // the denominator closes the cycle over all phases, so `product` is the
294 // stationary weight of arriving in phase r having started the cycle in
295 // phase i. Both denominators are structurally positive for non-negative
296 // inputs, since d = q0*q1*q2 < 1 whenever y_ij + 1 > 0.
297 // `next` is the second plane of the reference's PrOT array. Nothing reads it
298 // in the single-conditioning case solved here; it is kept because it is half
299 // of the state pair the chain produces and dropping it would leave the port
300 // silently unable to answer the multi-conditioning case.
301 auto prod_of_b = [&](std::size_t k) { return T(b[k] / (one - d[k])); };
302 Matrix<T> over(nStates, nStates, zero), next(nStates, nStates, zero);
303 for (std::size_t i0 = 0; i0 < nStates; ++i0) {
304 T temp = one;
305 for (std::size_t r0 = 0; r0 < nStates; ++r0)
306 if (r0 != i0) temp = T(temp * prod_of_b(r0));
307 T product = T(one / (one - (b[i0] * temp + d[i0])));
308 std::size_t r0 = i0;
309 for (;;) {
310 over(i0, r0) = T(c[i0] * product);
311 next(i0, r0) = T(a[i0] * product);
312 r0 = (r0 == 0) ? maxPhaseA : r0 - 1;
313 product = T(product * prod_of_b(r0));
314 if (r0 == i0) break;
315 }
316 }
317
318 // computeOvertaking with entA == entC. The client's next request leaves
319 // from its last phase, so nextProb puts all mass there; the y_aj ratio
320 // conditions on the request having been issued in phase i.
321 std::vector<T> nextProb(nStates, zero);
322 if (maxPhaseA >= 1) nextProb[maxPhaseA] = one;
323
324 T prOt = zero;
325 for (std::size_t i = 1; i <= maxPhaseA; ++i) {
326 if (clientPhases(i, 2) == zero) continue; // phase i never calls this server
327 T acc = zero;
328 for (std::size_t r = 1; r <= maxPhaseA; ++r) acc = T(acc + nextProb[r] * over(i, r));
329 // y_aj[i] cannot vanish here: it counts the same calls as clientPhases(i,2)
330 prOt = T(prOt + acc * (y_aj[0] / y_aj[i]));
331 }
332 return prOt;
333}
334
335// ---------------------------------------------------------------------------
336// ln_interlock_method
337// ---------------------------------------------------------------------------
338
339/** The interlock tracking method a run will use, and why it differs from the request. */
341 std::string method; ///< "ilrate", "refpath" or "none"
342 std::string why; ///< empty exactly when the request was honoured
343};
344
345/**
346 * Port of matlab/src/solvers/LN/ln_interlock_method.m, asked as a query.
347 *
348 * ONE QUERY, TWO CALLERS: SolverLN::build_layers asks it on the run path and
349 * records the answer, and SolverLN::interlock_method_for asks it before a solve,
350 * so a caller reading the option back is never told something the builder did
351 * not do. IT DOWNGRADES, IT DOES NOT REFUSE: whatever refpath cannot carry,
352 * 'ilrate' analyses exactly as it did before the option existed.
353 *
354 * @param requested `config.interlock_method`, case-insensitive; empty means 'ilrate'
355 * @param interlocking `config.interlocking`; false means no correction of any kind, 'none'
356 * @param lnmethod the resolved encoding ("srvn.cs", "moment3", ...)
357 */
358template <class T>
359LnInterlockChoice ln_interlock_method(const LqnStruct<T>& lqn, const std::string& requested,
360 bool interlocking, const std::string& lnmethod) {
362 out.method = "ilrate";
363 if (!requested.empty()) {
364 out.method = requested;
365 for (char& ch : out.method) ch = char(std::tolower(static_cast<unsigned char>(ch)));
366 }
367 if (out.method != "ilrate" && out.method != "refpath" && out.method != "none")
368 throw InputError("Unknown config.interlock_method '" + out.method +
369 "', use 'ilrate', 'refpath' or 'none'.");
370 // the master switch wins over the tracking method
371 if (!interlocking) {
372 out.method = "none";
373 return out;
374 }
375 if (out.method != "refpath") return out;
376 // refpath writes class switches into the routing of the client Delay, which
377 // only the srvn.cs encoding carries; flat.cs is refused too, see the reference
378 if (lnmethod != "srvn.cs") {
379 out.method = "ilrate";
380 out.why = "config.interlock_method='refpath' is carried by the 'srvn.cs' routing "
381 "encoding only; '" + lnmethod + "' falls back to 'ilrate'.";
382 return out;
383 }
384 // A phase-2 entry replies BEFORE phase 2 runs, so the caller sees
385 // servt_ph1 + prOvertake*servt_ph2, not the summed residence a hop charges.
386 for (std::size_t a = 1; a < lqn.actphase.size(); ++a)
387 if (lqn.actphase[a] > 1) {
388 out.method = "ilrate";
389 out.why = "config.interlock_method='refpath' does not yet carry second-phase "
390 "activities, whose caller-visible residence is servt_ph1 + "
391 "prOvertake*servt_ph2 and not the summed activity residence a hop stage "
392 "charges; falling back to 'ilrate'.";
393 return out;
394 }
395 // a routed call group's dispatch owns the client routing the re-expansion rewrites
396 if (!lqn.callgroups.empty()) {
397 out.method = "ilrate";
398 out.why = "config.interlock_method='refpath' does not carry routed call groups, whose "
399 "dispatch owns the client routing the re-expansion rewrites; falling back to "
400 "'ilrate'.";
401 return out;
402 }
403 return out;
404}
405
406} // namespace ln
407} // namespace line
408
409#endif // LINE_SOLVERS_LN_LQN_HELPERS_H
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
The exception types the port throws.
LayeredNetworkStruct, the flattened description of a layered queueing network.
Dense matrix and non-owning view.
CallType
Call kinds, with the values of MATLAB CallType.
Definition lang_types.h:469
void lqn_fwd_rendezvous(LqnStruct< T > &lqn)
Replace every forwarding chain reachable from a synchronous call by caller-side pseudo rendezvous cal...
Definition lqn_helpers.h:77
T lqn_overtake_markov(const Matrix< T > &clientPhases, const T &prVisit, const T &xj, const std::vector< T > &y_aj)
Overtaking probability from the LQNS phased-server Markov chain.
LnInterlockChoice ln_interlock_method(const LqnStruct< T > &lqn, const std::string &requested, bool interlocking, const std::string &lnmethod)
Port of matlab/src/solvers/LN/ln_interlock_method.m, asked as a query.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
Number-type abstraction for the templated API port.
The interlock tracking method a run will use, and why it differs from the request.
std::string method
"ilrate", "refpath" or "none"
std::string why
empty exactly when the request was honoured