LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
lqn_mol.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_API_LQN_MOL_H
6#define LINE_API_LQN_MOL_H
7
8/**
9 * @file
10 * @ingroup api_lqn
11 * Method of Layers on the SRVN decomposition of a layered queueing network
12 * whose entries carry no activity graph.
13 *
14 * Port of matlab/src/api/lqn/lqn_mol.m. A compact, self-contained
15 * reimplementation of the layered fixed point SolverLN runs, restricted to LQNs
16 * in which every entry binds exactly one activity and there are no activity
17 * precedences. It decomposes the model the way `lqns --srvn-layering` does --
18 * one submodel per processor and one per called task -- and sweeps them in the
19 * two phases of Rolia-Sevcik's Method of Layers: all software (task) submodels,
20 * then all hardware (processor) ones.
21 *
22 * Every submodel is a closed multiclass queueing network with ONE station and
23 * one class per client task, so it is solved by `pfqn_qdamva` rather than by
24 * building a Network. The surrogate client delay of SolverLN collapses into the
25 * think-time vector Z of that call.
26 *
27 * WHERE THIS DIFFERS FROM SolverLN's `srvn.cs`: a submodel here carries one
28 * class per client TASK with visit-weighted demands, where `srvn.cs` carries
29 * one class per activity and encodes the call multiplicities as routing. On an
30 * entry-only model the two agree on the structure and differ only in the
31 * aggregation, so the throughputs and processor utilizations track closely
32 * while entry response times spread more.
33 *
34 * SCOPE. Entry-only models. Activity graphs (fork/join, OR-branches, loops,
35 * second phases, forwarding), asynchronous calls, caches, setup tasks,
36 * admission constraints, replication and open arrivals are REFUSED, not
37 * approximated, and named when they are.
38 *
39 * Arithmetic: TRANSCENDENTAL-GATED, inherited from `pfqn_qdamva`.
40 */
41
42#include <algorithm>
43#include <cmath>
44#include <cstddef>
45#include <limits>
46#include <set>
47#include <string>
48#include <vector>
49
52#include "line/num/number.h"
53#include "line/util/error.h"
54#include "line/util/matrix.h"
55
56namespace line {
57namespace lqn {
58
59/** Everything `lqn_mol` reports beyond the four measure vectors. */
60template <class T>
61struct LqnMolInfo {
62 std::size_t iter = 0;
63 double resid = 0.0;
64 std::vector<T> servt; ///< (nidx+1) entry response time seen by a caller
65 std::vector<T> residt; ///< (nidx+1) entry processor residence
66 std::vector<T> callservt; ///< (ncalls+1) blocking time per call
67 std::vector<T> thinkt; ///< (nidx+1) task surrogate idle time
68 std::vector<T> share; ///< (nidx+1) entry share of its task's invocations
69 std::vector<std::size_t> hostLayers;
70 std::vector<std::size_t> taskLayers;
71};
72
73/**
74 * The four (nidx+1) vectors in the column convention SolverLN and LQNS report,
75 * so they line up with `LN(model).getAvgTable` cell for cell.
76 *
77 * | index | QN (QLen) | UN (Util) | RN (RespT) | TN (Tput) |
78 * |-------|-----------|-----------|------------|-----------|
79 * | host | NaN | processor utilization | NaN | NaN |
80 * | task | sum of entry T*S | sum of entry proc util | NaN | cycle rate |
81 * | entry | T*S | processor utilization | response time | throughput |
82 * | act | as its entry | as its entry | as its entry | as its entry |
83 */
84template <class T>
86 std::vector<T> QN, UN, RN, TN;
88};
89
90/** Tuning of the outer fixed point. */
92 std::size_t iter_max = 200;
93 double iter_tol = 1e-6;
94 double relax_factor = 0.5;
95};
96
97namespace mol_detail {
98
99/** GlobalConstants.FineTol, the reference's own "effectively zero". */
100inline double fine_tol() { return 1e-8; }
101
102/** CallType.toText, for the refusal messages. */
103inline const char* call_kind(CallType c) {
104 switch (c) {
105 case CallType::SYNC: return "synchronous";
106 case CallType::ASYNC: return "asynchronous";
107 case CallType::FWD: return "forwarding";
108 default: return "none";
109 }
110}
111
112/**
113 * Refuse every feature this decomposition does not represent, NAMING the
114 * element, rather than returning a number that quietly ignores it.
115 */
116template <class T>
117void lqn_mol_assert(const LqnStruct<T>& lsn) {
118 for (std::size_t eidx = lsn.eshift + 1; eidx <= lsn.eshift + lsn.nentries; ++eidx) {
119 const std::size_t n = (eidx < lsn.actsof.size()) ? lsn.actsof[eidx].size() : 0;
120 if (n != 1)
121 throw UnsupportedError("lqn_mol: entry " + lsn.hashnames[eidx] + " binds " +
122 std::to_string(n) +
123 " activities. lqn_mol solves entry-only models; use SolverLN "
124 "for an activity graph");
125 }
126 for (std::size_t a = 1; a <= lsn.nacts; ++a) {
127 const std::size_t aidx = lsn.ashift + a;
128 // A successor INSIDE the activity band is a precedence; an edge to an
129 // entry or a task is the ordinary binding every entry-only model has.
130 const std::vector<std::size_t> succ = lsn.graph.succ(aidx);
131 for (std::size_t k = 0; k < succ.size(); ++k)
132 if (succ[k] > lsn.ashift && succ[k] <= lsn.ashift + lsn.nacts)
133 throw UnsupportedError("lqn_mol: activity " + lsn.hashnames[aidx] +
134 " has an activity precedence. lqn_mol solves entry-only "
135 "models; use SolverLN for an activity graph");
136 if (a < lsn.actphase.size() && lsn.actphase[a] != 1)
137 throw UnsupportedError("lqn_mol: activity " + lsn.hashnames[aidx] + " is in phase " +
138 std::to_string(lsn.actphase[a]) +
139 ". lqn_mol supports phase 1 only");
140 }
141 for (std::size_t c = 1; c <= lsn.ncalls; ++c)
142 if (lsn.calltype[c] != CallType::SYNC)
143 throw UnsupportedError("lqn_mol: call " + lsn.callhashnames[c] + " is " +
144 std::string(mol_detail::call_kind(lsn.calltype[c])) +
145 ". lqn_mol supports synchronous calls only");
146 for (std::size_t idx = 1; idx <= lsn.tshift + lsn.ntasks; ++idx) {
147 if (idx < lsn.iscache.size() && lsn.iscache[idx])
148 throw UnsupportedError("lqn_mol: " + lsn.hashnames[idx] +
149 " is a cache task, which lqn_mol does not model");
150 if (idx < lsn.hassetup.size() && lsn.hassetup[idx])
151 throw UnsupportedError("lqn_mol: " + lsn.hashnames[idx] +
152 " has a setup time, which lqn_mol does not model");
153 if (idx < lsn.repl.size() && lsn.repl[idx] != 1.0)
154 throw UnsupportedError("lqn_mol: " + lsn.hashnames[idx] + " is replicated " +
155 std::to_string(static_cast<long long>(lsn.repl[idx])) +
156 " times, which lqn_mol does not model");
157 if (idx < lsn.lincon_A.size() && lsn.lincon_A[idx].rows() > 0)
158 throw UnsupportedError("lqn_mol: " + lsn.hashnames[idx] +
159 " carries an admission constraint, which lqn_mol does not "
160 "model");
161 const SchedStrategy s = lsn.sched[idx];
162 const bool ok = (s == SchedStrategy::PS || s == SchedStrategy::FCFS ||
163 s == SchedStrategy::INF ||
164 (idx > lsn.nhosts && s == SchedStrategy::REF));
165 if (!ok)
166 throw UnsupportedError("lqn_mol: " + lsn.hashnames[idx] + " is scheduled " +
167 std::string(sched_to_text(s)) +
168 ", which lqn_mol does not model");
169 }
170 if (!lsn.callgroups.empty())
171 throw UnsupportedError(
172 "lqn_mol: this model uses routed call groups, which lqn_mol does not model");
173 for (std::size_t eidx = lsn.eshift + 1; eidx <= lsn.eshift + lsn.nentries; ++eidx)
174 if (eidx < lsn.has_arrival.size() && lsn.has_arrival[eidx])
175 throw UnsupportedError("lqn_mol: entry " + lsn.hashnames[eidx] +
176 " has an open arrival. lqn_mol solves closed models only");
177}
178
179/**
180 * The queue-dependent rate multiplier row of a c-server station over a
181 * population of sum(N).
182 *
183 * `pfqn_lldfun` SKIPS a constant row, so a single server must come back as a
184 * row of ones and not as a scalar 1, or the multiserver term is never applied.
185 */
186template <class T>
187Matrix<T> mol_mu(const std::vector<T>& N, double c) {
188 double tot = 0.0;
189 for (std::size_t k = 0; k < N.size(); ++k) tot += num_traits<T>::to_double(N[k]);
190 const std::size_t smax = std::max<std::size_t>(
191 2, static_cast<std::size_t>(std::ceil(std::max(0.0, tot))));
192 Matrix<T> mu(1, smax, num_traits<T>::from_int(1));
193 if (std::isfinite(c) && c > 1.0)
194 for (std::size_t n = 1; n <= smax; ++n)
195 mu(0, n - 1) = num_traits<T>::from_double(std::min(static_cast<double>(n), c));
196 return mu;
197}
198
199} // namespace mol_detail
200
201/**
202 * @brief Method of Layers on the SRVN decomposition of a layered queueing
203 * network whose entries carry no activity graph.
204 *
205 * @param lsn LayeredNetworkStruct of an entry-only, closed, synchronous LQN
206 * @param options iteration cap, tolerance and under-relaxation factor
207 */
208template <class T>
210 static_assert(num_traits<T>::has_transcendental, "lqn_mol requires transcendental arithmetic");
211 mol_detail::lqn_mol_assert(lsn);
212
213 const T zero = num_traits<T>::from_int(0);
214 const std::size_t nidx = lsn.nidx, ncalls = lsn.ncalls;
215 const std::size_t e0 = lsn.eshift + 1, e1 = lsn.eshift + lsn.nentries;
216 const std::size_t t0 = lsn.tshift + 1, t1 = lsn.tshift + lsn.ntasks;
217 const double om = options.relax_factor;
218
219 // ---- static per-entry data: bound activity, host demand, owning task ----
220 std::vector<std::size_t> actof(nidx + 1, 0), taskof(nidx + 1, 0), hostof(nidx + 1, 0);
221 std::vector<T> dem(nidx + 1, zero);
222 for (std::size_t tidx = t0; tidx <= t1; ++tidx) hostof[tidx] = lsn.parent[tidx];
223 for (std::size_t eidx = e0; eidx <= e1; ++eidx) {
224 actof[eidx] = lsn.actsof[eidx][0];
225 const T d = lsn.hostdem[actof[eidx]].mean;
226 dem[eidx] = std::isnan(num_traits<T>::to_double(d)) ? zero : d;
227 taskof[eidx] = lsn.parent[eidx];
228 }
229
230 // ---- static per-call data ----------------------------------------------
231 std::vector<std::size_t> callsrc(ncalls + 1, 0), calldst(ncalls + 1, 0);
232 std::vector<T> cally(ncalls + 1, zero);
233 std::vector<std::size_t> entryOfAct(nidx + 1, 0);
234 for (std::size_t eidx = e0; eidx <= e1; ++eidx) entryOfAct[actof[eidx]] = eidx;
235 std::vector<std::vector<std::size_t>> callsFrom(nidx + 1), callsTo(nidx + 1);
236 for (std::size_t c = 1; c <= ncalls; ++c) {
237 callsrc[c] = entryOfAct[lsn.callpair_src[c]];
238 calldst[c] = lsn.callpair_dst[c];
239 const T y = lsn.callproc_mean[c];
240 cally[c] = std::isnan(num_traits<T>::to_double(y)) ? zero : y;
241 callsFrom[callsrc[c]].push_back(c);
242 callsTo[calldst[c]].push_back(c);
243 }
244
245 // ---- populations, from maxmult (mult is wrong for INF tasks) ------------
246 std::vector<double> npop(nidx + 1, 1.0);
247 for (std::size_t idx = 1; idx <= t1; ++idx) {
248 double m = (idx < lsn.maxmult.size()) ? lsn.maxmult[idx] : 1.0;
249 if (!std::isfinite(m) || m < 1.0) m = 1.0;
250 npop[idx] = m;
251 }
252
253 // ---- layer sets --------------------------------------------------------
254 // One hardware layer per populated host, one software layer per called
255 // non-reference task, as buildLayers.m draws them.
256 std::vector<std::size_t> hostLayers, taskLayers;
257 for (std::size_t hidx = 1; hidx <= lsn.nhosts; ++hidx)
258 if (!lsn.tasksof[hidx].empty()) hostLayers.push_back(hidx);
259 std::vector<bool> isCalled(nidx + 1, false);
260 for (std::size_t c = 1; c <= ncalls; ++c) isCalled[taskof[calldst[c]]] = true;
261 for (std::size_t tidx = t0; tidx <= t1; ++tidx)
262 if (!lsn.isref[tidx] && isCalled[tidx]) taskLayers.push_back(tidx);
263
264 // ---- fixed-point state -------------------------------------------------
265 std::vector<T> residt = dem, servt(nidx + 1, zero), thinkt(nidx + 1, zero),
266 share(nidx + 1, zero), Xtask(nidx + 1, zero), Xentry(nidx + 1, zero),
267 busyth(nidx + 1, zero), zref(nidx + 1, zero);
268 std::vector<T> callservt(ncalls + 1, zero);
269 for (std::size_t tidx = t0; tidx <= t1; ++tidx) {
270 // lqn_ref_thinktime: a reference task's declared think time, and zero
271 // for every other task and for a negative or non-finite one.
272 T z = zero;
273 if (lsn.isref[tidx] && tidx < lsn.think.size() && !lsn.think[tidx].disabled) {
274 z = lsn.think[tidx].mean;
275 const double zd = num_traits<T>::to_double(z);
276 if (!std::isfinite(zd) || zd < 0.0) z = zero;
277 }
278 zref[tidx] = z;
279 thinkt[tidx] = z;
280 const std::vector<std::size_t>& es = lsn.entriesof[tidx];
281 if (!es.empty())
282 for (std::size_t k = 0; k < es.size(); ++k)
283 share[es[k]] = num_traits<T>::from_double(1.0 / static_cast<double>(es.size()));
284 }
285 // Seed servt bottom-up over the call graph so a callee is priced before its
286 // caller; a cycle just leaves the residual demand seeded at 0.
287 for (std::size_t eidx = e0; eidx <= e1; ++eidx) servt[eidx] = dem[eidx];
288 for (std::size_t pass = 0; pass < std::max<std::size_t>(1, lsn.nentries); ++pass)
289 for (std::size_t eidx = e0; eidx <= e1; ++eidx) {
290 T s = residt[eidx];
291 for (std::size_t k = 0; k < callsFrom[eidx].size(); ++k) {
292 const std::size_t c = callsFrom[eidx][k];
293 s += T(cally[c] * servt[calldst[c]]);
294 }
295 servt[eidx] = s;
296 }
297 for (std::size_t c = 1; c <= ncalls; ++c) callservt[c] = servt[calldst[c]];
298
299 // ---- submodel solvers --------------------------------------------------
300
301 /* Time a thread of `tidx` spends away from `excl` in one cycle: its think
302 time, its own processor residence, and its blocking at every callee other
303 than `excl`. */
304 auto cycle_outside = [&](std::size_t tidx, std::size_t excl) -> T {
305 T z = thinkt[tidx];
306 for (std::size_t j = 0; j < lsn.entriesof[tidx].size(); ++j) {
307 const std::size_t eidx = lsn.entriesof[tidx][j];
308 T w = T(share[eidx] * residt[eidx]);
309 for (std::size_t k = 0; k < callsFrom[eidx].size(); ++k) {
310 const std::size_t c = callsFrom[eidx][k];
311 if (taskof[calldst[c]] != excl) w += T(share[eidx] * cally[c] * callservt[c]);
312 }
313 z += w;
314 }
315 return z;
316 };
317
318 /* LINE scales a station utilization into [0,1] whatever its multiplicity,
319 and reports busy SERVERS at an infinite server. */
320 auto host_servers = [&](std::size_t hidx) -> double {
321 return (lsn.sched[hidx] == SchedStrategy::INF) ? 1.0 : npop[hidx];
322 };
323
324 /* Reference tasks set the pace; every other rate follows from the call
325 rates, so the entries are visited in call-graph order until stable. */
326 auto throughputs = [&]() {
327 for (std::size_t t = t0; t <= t1; ++t) {
328 if (lsn.isref[t]) {
329 T cyc = thinkt[t];
330 for (std::size_t j = 0; j < lsn.entriesof[t].size(); ++j) {
331 const std::size_t eidx = lsn.entriesof[t][j];
332 cyc += T(share[eidx] * servt[eidx]);
333 }
334 Xtask[t] = (num_traits<T>::to_double(cyc) > mol_detail::fine_tol())
335 ? T(num_traits<T>::from_double(npop[t]) / cyc)
336 : zero;
337 for (std::size_t j = 0; j < lsn.entriesof[t].size(); ++j) {
338 const std::size_t eidx = lsn.entriesof[t][j];
339 Xentry[eidx] = T(Xtask[t] * share[eidx]);
340 }
341 } else {
342 Xtask[t] = zero;
343 for (std::size_t j = 0; j < lsn.entriesof[t].size(); ++j)
344 Xentry[lsn.entriesof[t][j]] = zero;
345 }
346 }
347 for (std::size_t pass = 0; pass < std::max<std::size_t>(1, lsn.ntasks); ++pass) {
348 for (std::size_t eidx = e0; eidx <= e1; ++eidx) {
349 if (lsn.isref[taskof[eidx]]) continue;
350 T x = zero;
351 for (std::size_t k = 0; k < callsTo[eidx].size(); ++k) {
352 const std::size_t c = callsTo[eidx][k];
353 x += T(cally[c] * Xentry[callsrc[c]]);
354 }
355 Xentry[eidx] = x;
356 }
357 for (std::size_t t = t0; t <= t1; ++t) {
358 if (lsn.isref[t]) continue;
359 const std::vector<std::size_t>& es = lsn.entriesof[t];
360 T sum = zero;
361 for (std::size_t j = 0; j < es.size(); ++j) sum += Xentry[es[j]];
362 Xtask[t] = sum;
363 if (num_traits<T>::to_double(sum) > mol_detail::fine_tol())
364 for (std::size_t j = 0; j < es.size(); ++j)
365 share[es[j]] = T(Xentry[es[j]] / sum);
366 }
367 }
368 // Mean busy threads, by Little's law over the entries the task serves.
369 // This is the occupancy the think-time closure needs, and it is exact
370 // given the throughputs -- unlike the layer AMVA's own U, which is
371 // X*L*g with g a reciprocal rate multiplier and not a server count.
372 for (std::size_t t = t0; t <= t1; ++t) {
373 T u = zero;
374 for (std::size_t j = 0; j < lsn.entriesof[t].size(); ++j) {
375 const std::size_t eidx = lsn.entriesof[t][j];
376 u += T(Xentry[eidx] * servt[eidx]);
377 }
378 busyth[t] = u;
379 }
380 };
381
382 std::size_t iter = 0;
383 double resid = std::numeric_limits<double>::infinity();
384 while (iter < options.iter_max) {
385 ++iter;
386 const std::vector<T> servt_prev = servt, thinkt_prev = thinkt;
387
388 // ---- phase 1: software layers (thread contention at each called task)
389 for (std::size_t li = 0; li < taskLayers.size(); ++li) {
390 const std::size_t tidx = taskLayers[li];
391 // The task is the station, its caller tasks the classes.
392 std::set<std::size_t> callerset;
393 for (std::size_t j = 0; j < lsn.entriesof[tidx].size(); ++j)
394 for (std::size_t k = 0; k < callsTo[lsn.entriesof[tidx][j]].size(); ++k)
395 callerset.insert(taskof[callsrc[callsTo[lsn.entriesof[tidx][j]][k]]]);
396 const std::vector<std::size_t> callers(callerset.begin(), callerset.end());
397 const std::size_t Kc = callers.size();
398 if (Kc == 0) continue;
399 std::vector<T> gcl(Kc, num_traits<T>::from_int(1));
400 if (lsn.sched[tidx] != SchedStrategy::INF) {
401 // An infinite-thread task never queues for a thread.
402 Matrix<T> L(1, Kc, zero);
403 std::vector<T> N(Kc, zero), Z(Kc, zero);
404 for (std::size_t k = 0; k < Kc; ++k) {
405 const std::size_t ctask = callers[k];
406 N[k] = num_traits<T>::from_double(npop[ctask]);
407 T d = zero;
408 for (std::size_t j = 0; j < lsn.entriesof[ctask].size(); ++j) {
409 const std::size_t eidx = lsn.entriesof[ctask][j];
410 for (std::size_t m = 0; m < callsFrom[eidx].size(); ++m) {
411 const std::size_t c = callsFrom[eidx][m];
412 if (taskof[calldst[c]] == tidx)
413 d += T(share[eidx] * cally[c] * servt[calldst[c]]);
414 }
415 }
416 L(0, k) = d;
417 Z[k] = cycle_outside(ctask, tidx);
418 }
419 const pfqn::QdAmvaResult<T> r =
420 pfqn::pfqn_qdamva(L, N, Z, mol_detail::mol_mu(N, npop[tidx]), Matrix<T>());
421 for (std::size_t k = 0; k < Kc; ++k)
422 if (num_traits<T>::to_double(L(0, k)) > mol_detail::fine_tol())
423 gcl[k] = T(r.R(0, k) / L(0, k));
424 }
425 for (std::size_t k = 0; k < Kc; ++k) {
426 const std::size_t ctask = callers[k];
427 for (std::size_t j = 0; j < lsn.entriesof[ctask].size(); ++j) {
428 const std::size_t eidx = lsn.entriesof[ctask][j];
429 for (std::size_t m = 0; m < callsFrom[eidx].size(); ++m) {
430 const std::size_t c = callsFrom[eidx][m];
431 if (taskof[calldst[c]] != tidx) continue;
432 const T newv = T(gcl[k] * servt[calldst[c]]);
433 callservt[c] = T(num_traits<T>::from_double(om) * newv +
434 num_traits<T>::from_double(1.0 - om) * callservt[c]);
435 }
436 }
437 }
438 }
439
440 // ---- phase 2: hardware layers (processor contention at each host) ---
441 for (std::size_t li = 0; li < hostLayers.size(); ++li) {
442 const std::size_t hidx = hostLayers[li];
443 // The processor is the station, its tasks the classes.
444 const std::vector<std::size_t>& tsks = lsn.tasksof[hidx];
445 const std::size_t Kt = tsks.size();
446 std::vector<T> f(Kt, num_traits<T>::from_int(1));
447 if (lsn.sched[hidx] != SchedStrategy::INF) {
448 // A delay processor never queues.
449 Matrix<T> L(1, Kt, zero);
450 std::vector<T> N(Kt, zero), Z(Kt, zero);
451 for (std::size_t k = 0; k < Kt; ++k) {
452 const std::size_t tidx = tsks[k];
453 N[k] = num_traits<T>::from_double(npop[tidx]);
454 T d = zero, z = thinkt[tidx];
455 for (std::size_t j = 0; j < lsn.entriesof[tidx].size(); ++j) {
456 const std::size_t eidx = lsn.entriesof[tidx][j];
457 d += T(share[eidx] * dem[eidx]);
458 for (std::size_t m = 0; m < callsFrom[eidx].size(); ++m) {
459 const std::size_t c = callsFrom[eidx][m];
460 z += T(share[eidx] * cally[c] * callservt[c]);
461 }
462 }
463 L(0, k) = d;
464 Z[k] = z;
465 }
466 const pfqn::QdAmvaResult<T> r =
467 pfqn::pfqn_qdamva(L, N, Z, mol_detail::mol_mu(N, npop[hidx]), Matrix<T>());
468 for (std::size_t k = 0; k < Kt; ++k)
469 if (num_traits<T>::to_double(L(0, k)) > mol_detail::fine_tol())
470 f[k] = T(r.R(0, k) / L(0, k));
471 }
472 for (std::size_t k = 0; k < Kt; ++k)
473 for (std::size_t j = 0; j < lsn.entriesof[tsks[k]].size(); ++j) {
474 const std::size_t eidx = lsn.entriesof[tsks[k]][j];
475 residt[eidx] = T(f[k] * dem[eidx]);
476 }
477 }
478
479 // ---- recompose entry service times ---------------------------------
480 for (std::size_t eidx = e0; eidx <= e1; ++eidx) {
481 T s = residt[eidx];
482 for (std::size_t k = 0; k < callsFrom[eidx].size(); ++k) {
483 const std::size_t c = callsFrom[eidx][k];
484 s += T(cally[c] * callservt[c]);
485 }
486 servt[eidx] = T(num_traits<T>::from_double(om) * s +
487 num_traits<T>::from_double(1.0 - om) * servt[eidx]);
488 }
489
490 // ---- throughputs, entry shares, think-time closure ------------------
491 throughputs();
492 for (std::size_t tidx = t0; tidx <= t1; ++tidx) {
493 if (lsn.isref[tidx]) {
494 thinkt[tidx] = zref[tidx];
495 continue;
496 }
497 if (num_traits<T>::to_double(Xtask[tidx]) <= mol_detail::fine_tol()) continue;
498 // Idle time of a thread per cycle. updateThinkTimes splits this into
499 // an INF arm (njobs - util) and a finite arm (njobs*abs(1-util))
500 // only because LINE reports busy SERVERS at an infinite server and a
501 // busy FRACTION at a finite one; carrying the count in both cases
502 // makes the two arms the same expression.
503 const double v = std::fabs(npop[tidx] - num_traits<T>::to_double(busyth[tidx])) /
504 num_traits<T>::to_double(Xtask[tidx]) -
505 num_traits<T>::to_double(zref[tidx]);
506 const T newz = num_traits<T>::from_double(std::max(0.0, v));
507 thinkt[tidx] = T(num_traits<T>::from_double(om) * newz +
508 num_traits<T>::from_double(1.0 - om) * thinkt[tidx]);
509 }
510
511 // Both halves of the state must settle: servt alone can sit still for
512 // an iteration while the think times are still moving.
513 resid = 0.0;
514 for (std::size_t eidx = e0; eidx <= e1; ++eidx) {
515 const double a = num_traits<T>::to_double(servt[eidx]);
516 const double b = num_traits<T>::to_double(servt_prev[eidx]);
517 resid = std::max(resid, std::fabs(a - b) / std::max(1.0, std::fabs(a)));
518 }
519 for (std::size_t tidx = t0; tidx <= t1; ++tidx) {
520 const double a = num_traits<T>::to_double(thinkt[tidx]);
521 const double b = num_traits<T>::to_double(thinkt_prev[tidx]);
522 resid = std::max(resid, std::fabs(a - b) / std::max(1.0, std::fabs(a)));
523 }
524 if (resid < options.iter_tol) break;
525 }
526 throughputs();
527
528 // ---- assemble the reported vectors -------------------------------------
529 const double nan = std::numeric_limits<double>::quiet_NaN();
530 LqnMolResult<T> out;
531 out.QN.assign(nidx + 1, num_traits<T>::from_double(nan));
532 out.UN = out.RN = out.TN = out.QN;
533 for (std::size_t eidx = e0; eidx <= e1; ++eidx) {
534 const std::size_t hidx = hostof[taskof[eidx]];
535 const T procutil =
536 T(Xentry[eidx] * dem[eidx] / num_traits<T>::from_double(host_servers(hidx)));
537 out.QN[eidx] = T(Xentry[eidx] * servt[eidx]);
538 out.UN[eidx] = procutil;
539 out.RN[eidx] = servt[eidx];
540 out.TN[eidx] = Xentry[eidx];
541 const std::size_t aidx = actof[eidx];
542 out.QN[aidx] = out.QN[eidx];
543 out.UN[aidx] = out.UN[eidx];
544 out.RN[aidx] = out.RN[eidx];
545 out.TN[aidx] = out.TN[eidx];
546 }
547 for (std::size_t tidx = t0; tidx <= t1; ++tidx) {
548 T q = zero, u = zero;
549 for (std::size_t j = 0; j < lsn.entriesof[tidx].size(); ++j) {
550 q += out.QN[lsn.entriesof[tidx][j]];
551 u += out.UN[lsn.entriesof[tidx][j]];
552 }
553 out.QN[tidx] = q;
554 out.UN[tidx] = u;
555 out.RN[tidx] = num_traits<T>::from_double(nan);
556 out.TN[tidx] = Xtask[tidx];
557 }
558 for (std::size_t hidx = 1; hidx <= lsn.nhosts; ++hidx) {
559 T u = zero;
560 for (std::size_t j = 0; j < lsn.tasksof[hidx].size(); ++j)
561 u += out.UN[lsn.tasksof[hidx][j]];
562 out.QN[hidx] = num_traits<T>::from_double(nan);
563 out.UN[hidx] = u;
564 out.RN[hidx] = num_traits<T>::from_double(nan);
565 // No throughput is defined at a processor, as in LQNS.
566 out.TN[hidx] = num_traits<T>::from_double(nan);
567 }
568
569 out.info.iter = iter;
570 out.info.resid = resid;
571 out.info.servt = servt;
572 out.info.residt = residt;
573 out.info.callservt = callservt;
574 out.info.thinkt = thinkt;
575 out.info.share = share;
576 out.info.hostLayers = hostLayers;
577 out.info.taskLayers = taskLayers;
578 return out;
579}
580
581} // namespace lqn
582} // namespace line
583
584#endif // LINE_API_LQN_MOL_H
UnsupportedError(const std::string &what)
Definition error.h:51
The exception types the port throws.
LayeredNetworkStruct, the flattened description of a layered queueing network.
Dense matrix and non-owning view.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
CallType
Call kinds, with the values of MATLAB CallType.
Definition lang_types.h:467
const char * sched_to_text(SchedStrategy s)
Definition lang_types.h:230
LqnMolResult< T > lqn_mol(const LqnStruct< T > &lsn, const LqnMolOptions &options=LqnMolOptions())
Method of Layers on the SRVN decomposition of a layered queueing network whose entries carry no activ...
Definition lqn_mol.h:209
QdAmvaResult< T > pfqn_qdamva(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z, const Matrix< T > &mu, const Matrix< T > &Q0, double tol=1e-6, std::size_t maxiter=10000)
QD-AMVA: queue-dependent approximate mean value analysis.
Definition pfqn_qdamva.h:89
Number-type abstraction for the templated API port.
QD-AMVA: queue-dependent approximate mean value analysis.
Everything lqn_mol reports beyond the four measure vectors.
Definition lqn_mol.h:61
std::vector< T > thinkt
(nidx+1) task surrogate idle time
Definition lqn_mol.h:67
std::vector< std::size_t > hostLayers
Definition lqn_mol.h:69
std::vector< T > residt
(nidx+1) entry processor residence
Definition lqn_mol.h:65
std::vector< T > servt
(nidx+1) entry response time seen by a caller
Definition lqn_mol.h:64
std::vector< T > callservt
(ncalls+1) blocking time per call
Definition lqn_mol.h:66
std::size_t iter
Definition lqn_mol.h:62
std::vector< T > share
(nidx+1) entry share of its task's invocations
Definition lqn_mol.h:68
std::vector< std::size_t > taskLayers
Definition lqn_mol.h:70
Tuning of the outer fixed point.
Definition lqn_mol.h:91
std::size_t iter_max
Definition lqn_mol.h:92
The four (nidx+1) vectors in the column convention SolverLN and LQNS report, so they line up with LN(...
Definition lqn_mol.h:85
std::vector< T > QN
Definition lqn_mol.h:86
std::vector< T > RN
Definition lqn_mol.h:86
LqnMolInfo< T > info
Definition lqn_mol.h:87
std::vector< T > UN
Definition lqn_mol.h:86
std::vector< T > TN
Definition lqn_mol.h:86
What pfqn_qdamva returns: the fixed point and how it was reached.
Definition pfqn_qdamva.h:69
Matrix< T > R
(M x R) per-class residence times, Q = X .* R
Definition pfqn_qdamva.h:73