LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
lqn_ref_routes.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_LQN_REF_ROUTES_H
6#define LINE_API_LQN_LQN_REF_ROUTES_H
7
8/**
9 * @file
10 * @ingroup api_lqn
11 * Synchronous call DAG carrying reference-task customers into a layer.
12 *
13 * Port of `matlab/src/api/lqn/lqn_ref_routes.m`. Resolves, for the caller set
14 * of one layer, the synchronous call graph along which reference (REF) task
15 * customers descend to those callers, and the mean number of times each entry
16 * and each call is invoked per REF cycle. SolverLN's `interlock_method='refpath'`
17 * reads it to merge the callers that are the same REF customers arriving by
18 * different routes into ONE client chain.
19 *
20 * Nodes are ENTRIES. Visits are computed TOPOLOGICALLY, v(u) = sum over parents
21 * of v(p)*w(a)*callmean, and routes are COUNTED in the same pass rather than
22 * enumerated. Only SYNC calls are followed: an ASYNC call is send-no-reply, and
23 * forwarding is already flattened into pseudo-SYNC arcs by lqn_fwd_rendezvous.
24 *
25 * INDEXING. Element and call indices are the 1-based ones of LqnStruct. The
26 * POSITIONS into `entries` (`LqnRefCall::from`, `::to`, `prefix_pos`) are
27 * 0-based, where the reference's are 1-based.
28 */
29
30#include <algorithm>
31#include <cmath>
32#include <cstddef>
33#include <cstdio>
34#include <limits>
35#include <string>
36#include <utility>
37#include <vector>
38
41#include "line/num/number.h"
42
43namespace line {
44namespace lqn {
45
46/** One row of `calls`: the reference's [cidx, fromPos, toPos, aidx, vCall]. */
47template <class T>
48struct LqnRefCall {
49 std::size_t cidx = 0; ///< call index
50 std::size_t from = 0; ///< 0-based position of the calling entry in `entries`
51 std::size_t to = 0; ///< 0-based position of the called entry in `entries`
52 std::size_t aidx = 0; ///< activity issuing the call
53 T vcall{}; ///< mean invocations of the call per REF cycle
54};
55
56/** One group, the reference's R(g): the DAG below one REF task. */
57template <class T>
59 std::size_t reftask = 0; ///< task index of the REF task at the root
60 bool head_is_caller = false; ///< the REF task is itself a caller of the layer
61 std::vector<std::size_t> members; ///< callers of the layer on the DAG, descent order
62 std::vector<std::size_t> entries; ///< every DAG entry, topological order, roots first
63 std::vector<std::size_t> etask; ///< lqn.parent of each entry
64 std::vector<bool> ismember; ///< that entry's task is a caller of the layer
65 std::vector<T> ventry; ///< mean invocations of each entry per REF cycle
66 /// per entry, (aidx, executions per invocation of the entry)
67 std::vector<std::vector<std::pair<std::size_t, T>>> actweight;
68 std::vector<LqnRefCall<T>> calls;
69 std::vector<std::size_t> prefix_pos; ///< 0-based positions forming the prefix, topological order
70 std::vector<bool> prefix_term; ///< that prefix position is a first caller
71 double npaths = 0.0; ///< distinct REF-to-caller routes, counted
72 /// min of lqn.maxmult over the DAG tasks. A DIAGNOSTIC: the chain population is never capped by it.
73 double poolmin = std::numeric_limits<double>::infinity();
74};
75
76/** What lqn_ref_routes returns: the groups, or a non-empty `why` and no groups. */
77template <class T>
79 std::vector<LqnRefGroup<T>> groups;
80 std::string why; ///< non-empty when the layer must fall back to another interlock method
81};
82
83namespace detail {
84
85/** One synchronous successor of an entry: [cidx, called entry, calling activity, callmean]. */
86template <class T>
87struct LqnRefSucc {
88 std::size_t cidx, to, aidx;
89 T mean;
90};
91
92/** Printable name of an LQN element, nameOf of the reference. */
93template <class T>
94std::string lqn_ref_name(const LqnStruct<T>& lqn, std::size_t idx) {
95 if (idx < lqn.hashnames.size() && !lqn.hashnames[idx].empty()) return lqn.hashnames[idx];
96 return "#" + std::to_string(idx);
97}
98
99/** MATLAB's %g. */
100inline std::string lqn_ref_fmt_g(double x) {
101 char buf[64];
102 std::snprintf(buf, sizeof(buf), "%g", x);
103 return buf;
104}
105
106/** Mean number of invocations carried by call CIDX; 1 when it is not finite. */
107template <class T>
108T lqn_ref_call_mean(const LqnStruct<T>& lqn, std::size_t cidx) {
109 if (cidx < lqn.callproc_mean.size()) {
110 const T m = lqn.callproc_mean[cidx];
111 if (std::isfinite(num_traits<T>::to_double(m))) return m;
112 }
113 return num_traits<T>::from_int(1);
114}
115
116/**
117 * Per calling ENTRY, its synchronous calls. The calling entry is NOT lqn.parent
118 * of the activity, which is its TASK: actsof is inverted over the entry range
119 * instead, the last entry listing an activity winning, as in the reference.
120 */
121template <class T>
122std::vector<std::vector<LqnRefSucc<T>>> lqn_ref_sync_successors(const LqnStruct<T>& lqn) {
123 std::vector<std::vector<LqnRefSucc<T>>> succ(lqn.nidx + 1);
124 std::vector<std::size_t> entry_of_act(lqn.nidx + 1, 0);
125 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
126 const std::size_t eidx = lqn.eshift + e;
127 for (std::size_t a : lqn.actsof[eidx])
128 if (a <= lqn.nidx) entry_of_act[a] = eidx;
129 }
130 for (std::size_t cidx = 1; cidx <= lqn.ncalls; ++cidx) {
131 if (lqn.calltype[cidx] != lang::CallType::SYNC) continue;
132 const std::size_t aidx = lqn.callpair_src[cidx];
133 const std::size_t eto = lqn.callpair_dst[cidx];
134 if (aidx < 1 || eto < 1 || aidx > lqn.nidx) continue;
135 const std::size_t efrom = entry_of_act[aidx];
136 if (efrom < 1) continue;
137 succ[efrom].push_back({cidx, eto, aidx, lqn_ref_call_mean(lqn, cidx)});
138 }
139 return succ;
140}
141
142/** Every entry reachable from task TIDX over SYNC calls, without pruning. */
143template <class T>
144std::vector<bool> lqn_ref_reachable(const LqnStruct<T>& lqn,
145 const std::vector<std::vector<LqnRefSucc<T>>>& succ,
146 std::size_t tidx) {
147 std::vector<bool> seen(lqn.nidx + 1, false);
148 std::vector<std::size_t> stack(lqn.entriesof[tidx].begin(), lqn.entriesof[tidx].end());
149 while (!stack.empty()) {
150 const std::size_t e = stack.back();
151 stack.pop_back();
152 if (seen[e]) continue;
153 seen[e] = true;
154 for (const LqnRefSucc<T>& s : succ[e]) stack.push_back(s.to);
155 }
156 return seen;
157}
158
159/**
160 * Mean executions of each activity of entry EIDX per invocation of that entry,
161 * propagated over lqn.graph so an OR-branch splits by its probabilities. An
162 * activity-graph loop is refused rather than truncated.
163 */
164template <class T>
165std::vector<std::pair<std::size_t, T>> lqn_ref_act_weights(const LqnStruct<T>& lqn,
166 std::size_t eidx, std::string& why) {
167 std::vector<std::pair<std::size_t, T>> aw;
168 const std::vector<std::size_t>& acts = lqn.actsof[eidx];
169 if (acts.empty()) return aw;
170 const T zero = num_traits<T>::from_int(0);
171 std::vector<std::size_t> nodeset;
172 nodeset.push_back(eidx);
173 nodeset.insert(nodeset.end(), acts.begin(), acts.end());
174 const std::size_t n = nodeset.size();
175 std::vector<std::size_t> pos(lqn.nidx + 1, 0); // 1-based, 0 = absent
176 for (std::size_t i = 0; i < n; ++i) pos[nodeset[i]] = i + 1;
177 std::vector<std::vector<T>> A(n, std::vector<T>(n, zero));
178 for (std::size_t i = 0; i < n; ++i)
179 for (std::size_t v : lqn.graph.succ(nodeset[i]))
180 if (v <= lqn.nidx && pos[v] > 0) A[i][pos[v] - 1] = lqn.graph.get(nodeset[i], v);
181 std::vector<long> remaining(n, 0);
182 for (std::size_t j = 0; j < n; ++j)
183 for (std::size_t i = 0; i < n; ++i)
184 if (A[i][j] > zero) ++remaining[j];
185 remaining[0] = 0; // the entry is the source
186 std::vector<T> w(n, zero);
187 w[0] = num_traits<T>::from_int(1);
188 std::vector<std::size_t> queue;
189 for (std::size_t j = 0; j < n; ++j)
190 if (remaining[j] == 0) queue.push_back(j);
191 std::vector<bool> done(n, false);
192 std::size_t ndone = 0, head = 0;
193 while (head < queue.size()) {
194 const std::size_t i = queue[head++];
195 if (done[i]) continue;
196 done[i] = true;
197 ++ndone;
198 for (std::size_t j = 0; j < n; ++j) {
199 if (!(A[i][j] > zero)) continue;
200 w[j] = T(w[j] + w[i] * A[i][j]);
201 --remaining[j];
202 if (remaining[j] <= 0 && !done[j]) queue.push_back(j);
203 }
204 }
205 if (ndone < n) {
206 why = "the activity graph of entry '" + lqn_ref_name(lqn, eidx) + "' contains a loop";
207 return {};
208 }
209 for (std::size_t i = 1; i < n; ++i) aw.emplace_back(nodeset[i], w[i]);
210 return aw;
211}
212
213/** One group: the DAG below REF task R, its visits, and the prefix above the first callers. */
214template <class T>
215bool lqn_ref_build_group(const LqnStruct<T>& lqn,
216 const std::vector<std::vector<LqnRefSucc<T>>>& succ, std::size_t r,
217 const std::vector<bool>& is_caller, double maxpaths,
218 const std::vector<std::size_t>& server_set, LqnRefGroup<T>& g,
219 std::string& why) {
220 const T zero = num_traits<T>::from_int(0);
221 const std::vector<std::size_t>& roots = lqn.entriesof[r];
222 if (roots.empty()) return false;
223
224 // Depth-first sweep with an on-stack marker: a back edge is REFUSED, since a
225 // cycle makes v(u) a geometric series the chain would have to express as a
226 // self-loop through the layer's own server.
227 enum : int { WHITE = 0, GREY = 1, BLACK = 2 };
228 std::vector<int> color(lqn.nidx + 1, WHITE);
229 std::vector<std::size_t> post;
230 for (std::size_t e0 : roots) {
231 if (color[e0] != WHITE) continue;
232 std::vector<std::pair<std::size_t, std::size_t>> stack{{e0, 0}};
233 while (!stack.empty()) {
234 const std::size_t u = stack.back().first;
235 const std::size_t ci = stack.back().second;
236 if (ci == 0) color[u] = GREY;
237 if (ci < succ[u].size()) {
238 stack.back().second = ci + 1;
239 const std::size_t v = succ[u][ci].to;
240 if (color[v] == GREY) {
241 why = "the synchronous call graph below '" + lqn_ref_name(lqn, v) +
242 "' is cyclic";
243 return false;
244 } else if (color[v] == WHITE) {
245 stack.emplace_back(v, 0);
246 }
247 } else {
248 color[u] = BLACK;
249 post.push_back(u);
250 stack.pop_back();
251 }
252 }
253 }
254 const std::vector<std::size_t> entries(post.rbegin(), post.rend()); // topological order
255 if (entries.empty()) return false;
256 const std::size_t nE = entries.size();
257 std::vector<std::size_t> pos(lqn.nidx + 1, 0); // 1-based, 0 = absent
258 for (std::size_t i = 0; i < nE; ++i) pos[entries[i]] = i + 1;
259 std::vector<std::size_t> etask(nE);
260 std::vector<bool> ismem(nE);
261 bool anymem = false;
262 for (std::size_t i = 0; i < nE; ++i) {
263 etask[i] = lqn.parent[entries[i]];
264 ismem[i] = is_caller[etask[i]];
265 anymem = anymem || ismem[i];
266 }
267 if (!anymem) return false; // this REF reaches none of the callers
268
269 // Per-entry activity weights, then the call list; calls carry the
270 // per-invocation weight w*callmean until the visits below rescale them.
271 std::vector<std::vector<std::pair<std::size_t, T>>> actweight(nE);
272 std::vector<LqnRefCall<T>> calls;
273 for (std::size_t i = 0; i < nE; ++i) {
274 const std::size_t u = entries[i];
275 std::string awwhy;
276 actweight[i] = lqn_ref_act_weights(lqn, u, awwhy);
277 if (!awwhy.empty()) {
278 why = awwhy;
279 return false;
280 }
281 for (const LqnRefSucc<T>& s : succ[u]) {
282 if (pos[s.to] == 0) continue;
283 T w = zero;
284 for (const std::pair<std::size_t, T>& aw : actweight[i])
285 if (aw.first == s.aidx) {
286 w = aw.second;
287 break;
288 }
289 calls.push_back({s.cidx, i, pos[s.to] - 1, s.aidx, T(w * s.mean)});
290 }
291 }
292
293 // Topological visits: a root entry is visited 1/nentries times per REF cycle
294 std::vector<T> ventry(nE, zero);
295 std::vector<std::size_t> rootpos;
296 for (std::size_t e : roots)
297 if (pos[e] > 0) rootpos.push_back(pos[e] - 1);
298 const T rootshare =
299 T(num_traits<T>::from_int(1) / num_traits<T>::from_int(int(roots.size())));
300 for (std::size_t p : rootpos) ventry[p] = rootshare;
301 for (std::size_t i = 0; i < nE; ++i)
302 for (const LqnRefCall<T>& c : calls)
303 if (c.from == i) ventry[c.to] = T(ventry[c.to] + ventry[i] * c.vcall);
304 for (LqnRefCall<T>& c : calls) c.vcall = T(ventry[c.from] * c.vcall);
305
306 // The prefix: entries reachable from a root without passing THROUGH a caller
307 std::vector<bool> in_prefix(nE, false), pref_term(nE, false);
308 std::vector<double> npath(nE, 0.0);
309 for (std::size_t p : rootpos) {
310 in_prefix[p] = true;
311 npath[p] = 1.0;
312 }
313 for (std::size_t i = 0; i < nE; ++i) {
314 if (!in_prefix[i]) continue;
315 if (ismem[i]) {
316 pref_term[i] = true;
317 continue; // do not descend past a caller
318 }
319 // a HOP whose task is a server of this layer would be in two places at once
320 for (std::size_t s : server_set)
321 if (s == etask[i]) {
322 why = "task '" + lqn_ref_name(lqn, etask[i]) +
323 "' is both an intermediate on the reference path and a server of this "
324 "layer";
325 return false;
326 }
327 for (const LqnRefCall<T>& c : calls)
328 if (c.from == i) {
329 in_prefix[c.to] = true;
330 npath[c.to] += npath[i];
331 }
332 }
333 double np = 0.0;
334 for (std::size_t i = 0; i < nE; ++i)
335 if (pref_term[i]) np += npath[i];
336 if (np > maxpaths) {
337 why = "the reference path into this layer carries " + lqn_ref_fmt_g(np) +
338 " distinct routes, above config.interlock_maxpaths = " + lqn_ref_fmt_g(maxpaths);
339 return false;
340 }
341
342 g = LqnRefGroup<T>();
343 g.reftask = r;
344 g.head_is_caller = is_caller[r];
345 for (std::size_t i = 0; i < nE; ++i) {
346 if (!ismem[i]) continue;
347 bool seen = false;
348 for (std::size_t t : g.members) seen = seen || t == etask[i];
349 if (!seen) g.members.push_back(etask[i]);
350 }
351 g.entries = entries;
352 g.etask = etask;
353 g.ismember = ismem;
354 g.ventry = ventry;
355 g.actweight = actweight;
356 g.calls = calls;
357 for (std::size_t i = 0; i < nE; ++i)
358 if (in_prefix[i]) {
359 g.prefix_pos.push_back(i);
360 g.prefix_term.push_back(pref_term[i]);
361 }
362 g.npaths = np;
363 // DIAGNOSTIC ONLY: capping the chain at this would delete customers from the REF think stage
364 for (std::size_t t : etask)
365 if (t > 0 && t < lqn.maxmult.size()) g.poolmin = std::min(g.poolmin, lqn.maxmult[t]);
366 return true;
367}
368
369} // namespace detail
370
371/**
372 * Resolve the reference routes into the layer whose callers are CALLERS.
373 *
374 * @param lqn the layered struct (after lqn_fwd_rendezvous, as SolverLN holds it)
375 * @param callers task indices that call the layer's server
376 * @param maxpaths refuse the layer above this many REF routes into it (default 32)
377 * @param server_set server elements of the layer; a prefix node whose task is one of them refuses
378 *
379 * A caller reachable from two REF tasks is two INDEPENDENT customer pools, and
380 * the whole LAYER falls back rather than that caller alone: a refused caller
381 * may lie on another group's path, which would count its threads twice.
382 */
383template <class T>
384LqnRefRoutes<T> lqn_ref_routes(const LqnStruct<T>& lqn, const std::vector<std::size_t>& callers,
385 double maxpaths = 32.0,
386 const std::vector<std::size_t>& server_set = {}) {
387 LqnRefRoutes<T> out;
388 if (lqn.ncalls == 0 || callers.empty()) return out;
389 std::vector<bool> is_caller(lqn.nidx + 1, false);
390 for (std::size_t c : callers)
391 if (c <= lqn.nidx) is_caller[c] = true;
392 const std::vector<std::vector<detail::LqnRefSucc<T>>> succ =
393 detail::lqn_ref_sync_successors(lqn);
394
395 std::vector<std::size_t> reftasks;
396 for (std::size_t t = 1; t <= lqn.ntasks; ++t)
397 if (lqn.isref[lqn.tshift + t]) reftasks.push_back(lqn.tshift + t);
398 if (reftasks.empty()) return out;
399
400 std::vector<int> nref_of(lqn.nidx + 1, 0);
401 for (std::size_t r : reftasks) {
402 const std::vector<bool> seen = detail::lqn_ref_reachable(lqn, succ, r);
403 std::vector<bool> mem(lqn.nidx + 1, false);
404 for (std::size_t e = 1; e <= lqn.nidx; ++e)
405 if (seen[e] && is_caller[lqn.parent[e]]) mem[lqn.parent[e]] = true;
406 if (is_caller[r]) mem[r] = true;
407 for (std::size_t t = 1; t <= lqn.nidx; ++t)
408 if (mem[t]) ++nref_of[t];
409 }
410 for (std::size_t t = 1; t <= lqn.nidx; ++t)
411 if (nref_of[t] > 1) {
412 out.why = "task '" + detail::lqn_ref_name(lqn, t) + "' is reachable from " +
413 std::to_string(nref_of[t]) +
414 " reference tasks, whose customer pools are independent";
415 return out;
416 }
417
418 for (std::size_t r : reftasks) {
420 std::string gwhy;
421 const bool ok =
422 detail::lqn_ref_build_group(lqn, succ, r, is_caller, maxpaths, server_set, g, gwhy);
423 if (!gwhy.empty()) {
424 out.why = gwhy;
425 out.groups.clear();
426 return out;
427 }
428 if (ok) out.groups.push_back(std::move(g));
429 }
430 return out;
431}
432
433} // namespace lqn
434} // namespace line
435
436#endif // LINE_API_LQN_LQN_REF_ROUTES_H
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
LayeredNetworkStruct, the flattened description of a layered queueing network.
LqnRefRoutes< T > lqn_ref_routes(const LqnStruct< T > &lqn, const std::vector< std::size_t > &callers, double maxpaths=32.0, const std::vector< std::size_t > &server_set={})
Resolve the reference routes into the layer whose callers are CALLERS.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
Number-type abstraction for the templated API port.
One row of calls: the reference's [cidx, fromPos, toPos, aidx, vCall].
std::size_t aidx
activity issuing the call
std::size_t cidx
call index
T vcall
mean invocations of the call per REF cycle
std::size_t from
0-based position of the calling entry in entries
std::size_t to
0-based position of the called entry in entries
One group, the reference's R(g): the DAG below one REF task.
std::vector< T > ventry
mean invocations of each entry per REF cycle
std::vector< std::size_t > members
callers of the layer on the DAG, descent order
std::vector< bool > prefix_term
that prefix position is a first caller
std::vector< std::size_t > entries
every DAG entry, topological order, roots first
double poolmin
min of lqn.maxmult over the DAG tasks. A DIAGNOSTIC: the chain population is never capped by it.
bool head_is_caller
the REF task is itself a caller of the layer
double npaths
distinct REF-to-caller routes, counted
std::vector< std::size_t > etask
lqn.parent of each entry
std::vector< std::size_t > prefix_pos
0-based positions forming the prefix, topological order
std::size_t reftask
task index of the REF task at the root
std::vector< bool > ismember
that entry's task is a caller of the layer
std::vector< LqnRefCall< T > > calls
std::vector< std::vector< std::pair< std::size_t, T > > > actweight
per entry, (aidx, executions per invocation of the entry)
What lqn_ref_routes returns: the groups, or a non-empty why and no groups.
std::vector< LqnRefGroup< T > > groups
std::string why
non-empty when the layer must fall back to another interlock method