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 <cmath>
30#include <cstddef>
31#include <string>
32#include <vector>
33
35#include "line/num/number.h"
36#include "line/util/error.h"
37#include "line/util/matrix.h"
38
39namespace line {
40namespace ln {
41
42using lang::CallType;
43using lqn::LqnStruct;
44
45// ---------------------------------------------------------------------------
46// lqn_fwd_rendezvous
47// ---------------------------------------------------------------------------
48
49/**
50 * Replace every forwarding chain reachable from a synchronous call by
51 * caller-side pseudo rendezvous calls to the forwarding targets.
52 *
53 * Port of LQNS Phase::addForwardingRendezvous (phase.cc). A forwarded call
54 * blocks the original caller until the LAST task in the chain replies, so the
55 * caller's blocking time spans the chain, not just the entry it named. Rather
56 * than teach every downstream stage what a FWD arc means, the chain is
57 * flattened here into ordinary SYNC arcs from the original calling activity to
58 * each entry on the chain, each with mean equal to the original call mean times
59 * the product of the forwarding probabilities on the path to it. Layer
60 * construction, think times, populations and the interlock analysis then see
61 * plain rendezvous arcs and need no forwarding case at all. This is also what
62 * LQNS does: interlock.cc drops FWD arcs outright ("Drop forward -- keep rnv")
63 * and accounts for forwarding only through these pseudo arcs.
64 *
65 * The FWD calls survive in the struct but must not contribute blocking anywhere
66 * after this point, or the chain is charged twice.
67 *
68 * Asynchronous calls into a forwarding chain are left alone: LQNS breaks the
69 * backward search at a send-no-reply, since nobody is blocked waiting for it.
70 *
71 * The MATLAB version also rebuilds the Geometric process descriptor of each
72 * rewritten call. There is nothing to port: LqnStruct carries only
73 * callproc_mean, which is the only field SolverLN reads.
74 */
75template <class T>
77 const T zero = num_traits<T>::from_int(0);
78 const T one = num_traits<T>::from_int(1);
79
80 bool any_fwd = false;
81 for (std::size_t c = 1; c <= lqn.ncalls; ++c)
82 if (lqn.calltype[c] == CallType::FWD) any_fwd = true;
83 if (!any_fwd) return;
84
85 // Frozen: the pseudo calls appended below are themselves SYNC, and
86 // rewriting them again would raise the chain to a power of its own
87 // probabilities.
88 const std::size_t ncalls0 = lqn.ncalls;
89
90 for (std::size_t cidx = 1; cidx <= ncalls0; ++cidx) {
91 if (lqn.calltype[cidx] != CallType::SYNC) continue;
92 const std::size_t aidx = lqn.callpair_src[cidx];
93 const std::size_t tidx = lqn.parent[aidx];
94 const T base_mean = lqn.callproc_mean[cidx];
95 if (base_mean <= zero) continue;
96
97 // breadth-first walk of the forwarding chain out of the sync target,
98 // carrying the probability of the path that reached each entry
99 std::vector<std::size_t> frontier{lqn.callpair_dst[cidx]};
100 std::vector<T> probs{one};
101 std::vector<std::size_t> visited;
102 auto seen = [](const std::vector<std::size_t>& v, std::size_t x) {
103 for (std::size_t e : v)
104 if (e == x) return true;
105 return false;
106 };
107
108 while (!frontier.empty()) {
109 const std::size_t eidx = frontier.front();
110 const T p_path = probs.front();
111 frontier.erase(frontier.begin());
112 probs.erase(probs.begin());
113 if (seen(visited, eidx)) continue;
114 visited.push_back(eidx);
115
116 for (std::size_t fcidx = 1; fcidx <= ncalls0; ++fcidx) {
117 if (lqn.calltype[fcidx] != CallType::FWD) continue;
118 if (lqn.callpair_src[fcidx] != eidx) continue;
119 const T fprob = lqn.callproc_mean[fcidx];
120 const std::size_t tgt = lqn.callpair_dst[fcidx];
121 const T pseudo_mean = T(base_mean * p_path * fprob);
122
123 // A chain that comes back to the caller's own task carries no
124 // pseudo arc: the caller would appear as its own client.
125 if (pseudo_mean > zero && lqn.parent[tgt] != tidx) {
126 // see _kb/06-solver-catalog.md (LN section) for rationale
127 std::size_t mrow = 0;
128 for (std::size_t scan = 1; scan <= lqn.ncalls; ++scan)
129 if (lqn.calltype[scan] == CallType::SYNC &&
130 lqn.callpair_src[scan] == aidx && lqn.callpair_dst[scan] == tgt) {
131 mrow = scan;
132 break;
133 }
134 if (mrow > 0) {
135 // an arc already exists, so the forwarded work is extra
136 // visits on it rather than a second parallel class
137 lqn.callproc_mean[mrow] = T(lqn.callproc_mean[mrow] + pseudo_mean);
138 } else {
139 const std::size_t ncall = lqn.ncalls + 1;
140 lqn.ncalls = ncall;
141 const std::size_t target_tidx = lqn.parent[tgt];
142 lqn.calltype.push_back(CallType::SYNC);
143 lqn.callpair_src.push_back(aidx);
144 lqn.callpair_dst.push_back(tgt);
145 lqn.callproc_mean.push_back(pseudo_mean);
146 lqn.callnames.push_back(lqn.names[aidx] + "=>" + lqn.names[tgt]);
147 lqn.callhashnames.push_back(lqn.hashnames[aidx] + "=>" +
148 lqn.hashnames[tgt]);
149 lqn.callsof[aidx].push_back(ncall);
150 lqn.iscaller.set(tidx, target_tidx);
151 lqn.iscaller.set(aidx, target_tidx);
152 lqn.iscaller.set(tidx, tgt);
153 lqn.iscaller.set(aidx, tgt);
154 lqn.issynccaller.set(tidx, target_tidx);
155 lqn.issynccaller.set(aidx, target_tidx);
156 lqn.issynccaller.set(tidx, tgt);
157 lqn.issynccaller.set(aidx, tgt);
158 lqn.graph.set(aidx, tgt, one);
159 lqn.taskgraph.set(tidx, target_tidx, one);
160 }
161 }
162
163 if (!seen(visited, tgt) && !seen(frontier, tgt)) {
164 frontier.push_back(tgt);
165 probs.push_back(T(p_path * fprob));
166 }
167 }
168 }
169 }
170}
171
172// ---------------------------------------------------------------------------
173// lqn_overtake_markov
174// ---------------------------------------------------------------------------
175
176namespace detail {
177
178/** True when x is a finite value of T; only a floating T can fail this. */
179template <class T>
180bool ln_finite(const T& x) {
181 return std::isfinite(num_traits<T>::to_double(x));
182}
183
184/**
185 * Transition rates of one client phase in the LQNS slice chain (slice.cc
186 * setRates). The four outputs are the probabilities of, respectively, staying
187 * in the client phase (a), completing it and revisiting the server (b),
188 * catching the server in the tested phase (c), and being held at another server
189 * first (d).
190 */
191template <class T>
192void ln_set_rates(const T& xj, const T& prA, const T& nSlices, const T& service, const T& y_ij,
193 const T& y_ik, const T& t_k, T& a, T& b, T& c, T& d) {
194 const T zero = num_traits<T>::from_int(0);
195 const T one = num_traits<T>::from_int(1);
196
197 const T y_sum = T(y_ij + y_ik + one);
198 const T slice = nSlices != zero ? T(service / nSlices) : zero;
199
200 T q0, q1, q3, q5;
201 T temp = T(xj + t_k);
202 if (!ln_finite(temp)) {
203 q0 = one;
204 q3 = zero;
205 } else if (temp != zero) {
206 q0 = T(xj / temp);
207 q3 = T(t_k / temp);
208 } else {
209 q0 = zero;
210 q3 = one;
211 }
212 temp = T(xj + slice);
213 if (!ln_finite(temp)) {
214 q1 = zero;
215 q5 = one;
216 } else if (temp != zero) {
217 q1 = T(xj / temp);
218 q5 = T(slice / temp);
219 } else {
220 q1 = one;
221 q5 = zero;
222 }
223 const T q2 = T(y_ik / y_sum), q4 = T(y_ij / y_sum), q6 = T(prA / y_sum);
224
225 a = T(q5 + q1 * q2 * q3);
226 b = T(q1 * q6);
227 c = T(q1 * q4);
228 d = T(q0 * q1 * q2);
229}
230
231} // namespace detail
232
233/**
234 * Overtaking probability from the LQNS phased-server Markov chain.
235 *
236 * Layer-1 port of LQNS V6 (slice.cc setRates and prOvertakingStates,
237 * overtake.cc computeOvertaking) for the single-conditioning case, where the
238 * calling entry and the conditioning entry coincide. Overtaking is the event
239 * that a client's next request reaches the server while the server is still
240 * running the second phase of the PREVIOUS request from the same client: the
241 * early reply released the client, so two of its requests can be in flight at
242 * once and the later one can pass the earlier one. `lqns -t overtaking` prints
243 * the same quantity; on its 31-overtaking model, phase 2 of the server gives
244 * 0.5.
245 *
246 * The chain is over client phases 0..maxPhaseA, phase 0 being the client's
247 * think slice. Row p of `clientPhases` is
248 * [nSlices, service, y_ij, y_ik, t_k]
249 * with nSlices = 1 + the rendezvous calls made in phase p (the slice count the
250 * phase is chopped into), service = total host residence of the phase, y_ij =
251 * calls to the tested server's task, y_ik = calls to any other task, and t_k =
252 * mean time spent at those other tasks. Row 0's service is the think time.
253 * `xj` is the server's residence time in the phase being tested, `prVisit` the
254 * client entry's visit probability, and y_aj[0] the client's total calls to the
255 * server with y_aj[i] the calls made in client phase i.
256 *
257 * Every rate is a ratio of times (xj/(xj+slice) and so on), so the result is
258 * invariant to a common rescaling of all the time inputs, as a probability must
259 * be.
260 *
261 * Reference: Franks and Woodside, "Effectiveness of early replies in
262 * client-server systems", Perf. Eval. 36 (1999).
263 */
264template <class T>
265T lqn_overtake_markov(const Matrix<T>& clientPhases, const T& prVisit, const T& xj,
266 const std::vector<T>& y_aj) {
267 const T zero = num_traits<T>::from_int(0);
268 const T one = num_traits<T>::from_int(1);
269
270 if (clientPhases.rows() < 1 || clientPhases.cols() < 5)
271 throw InputError(
272 "lqn_overtake_markov: clientPhases must have five columns "
273 "[nSlices service y_ij y_ik t_k] and one row per client phase 0..maxPhaseA");
274 const std::size_t nStates = clientPhases.rows();
275 const std::size_t maxPhaseA = nStates - 1;
276 if (y_aj.size() < nStates)
277 throw InputError(
278 "lqn_overtake_markov: y_aj must hold the total call count and one entry per client "
279 "phase");
280
281 // setRates for each client phase. prVisit applies only to the last phase,
282 // which is the one that ends the client cycle and issues the next request.
283 std::vector<T> a(nStates, zero), b(nStates, zero), c(nStates, zero), d(nStates, zero);
284 for (std::size_t p = 0; p < nStates; ++p) {
285 const T prA = (p == maxPhaseA) ? prVisit : one;
286 detail::ln_set_rates(xj, prA, clientPhases(p, 0), clientPhases(p, 1), clientPhases(p, 2),
287 clientPhases(p, 3), clientPhases(p, 4), a[p], b[p], c[p], d[p]);
288 }
289
290 // prOvertakingStates. prod_of_b folds the self-loop at a phase (probability
291 // d of a detour to another server) into the phase's forward probability;
292 // the denominator closes the cycle over all phases, so `product` is the
293 // stationary weight of arriving in phase r having started the cycle in
294 // phase i. Both denominators are structurally positive for non-negative
295 // inputs, since d = q0*q1*q2 < 1 whenever y_ij + 1 > 0.
296 // `next` is the second plane of the reference's PrOT array. Nothing reads it
297 // in the single-conditioning case solved here; it is kept because it is half
298 // of the state pair the chain produces and dropping it would leave the port
299 // silently unable to answer the multi-conditioning case.
300 auto prod_of_b = [&](std::size_t k) { return T(b[k] / (one - d[k])); };
301 Matrix<T> over(nStates, nStates, zero), next(nStates, nStates, zero);
302 for (std::size_t i0 = 0; i0 < nStates; ++i0) {
303 T temp = one;
304 for (std::size_t r0 = 0; r0 < nStates; ++r0)
305 if (r0 != i0) temp = T(temp * prod_of_b(r0));
306 T product = T(one / (one - (b[i0] * temp + d[i0])));
307 std::size_t r0 = i0;
308 for (;;) {
309 over(i0, r0) = T(c[i0] * product);
310 next(i0, r0) = T(a[i0] * product);
311 r0 = (r0 == 0) ? maxPhaseA : r0 - 1;
312 product = T(product * prod_of_b(r0));
313 if (r0 == i0) break;
314 }
315 }
316
317 // computeOvertaking with entA == entC. The client's next request leaves
318 // from its last phase, so nextProb puts all mass there; the y_aj ratio
319 // conditions on the request having been issued in phase i.
320 std::vector<T> nextProb(nStates, zero);
321 if (maxPhaseA >= 1) nextProb[maxPhaseA] = one;
322
323 T prOt = zero;
324 for (std::size_t i = 1; i <= maxPhaseA; ++i) {
325 if (clientPhases(i, 2) == zero) continue; // phase i never calls this server
326 T acc = zero;
327 for (std::size_t r = 1; r <= maxPhaseA; ++r) acc = T(acc + nextProb[r] * over(i, r));
328 // y_aj[i] cannot vanish here: it counts the same calls as clientPhases(i,2)
329 prOt = T(prOt + acc * (y_aj[0] / y_aj[i]));
330 }
331 return prOt;
332}
333
334} // namespace ln
335} // namespace line
336
337#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:467
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:76
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.
Number-type abstraction for the templated API port.