LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
ag_exec.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_AG_AG_EXEC_H
6#define LINE_SOLVERS_AG_AG_EXEC_H
7
8/**
9 * @file ag_exec.h
10 * @ingroup line_solvers
11 * @brief Execution backends of the reversed-rate fixed point.
12 *
13 * WHAT MAKES THIS SAFE IS THE DECOMPOSITION, NOT THE SCHEDULING. Agent k's
14 * generator is
15 *
16 * Q_k(x) = L_k + sum_{c passive at k} x_c Pb_c
17 *
18 * so an agent reads the rest of the model only through the scalar reversed rates
19 * x, and it writes only its own slot of the sweep's output. The sweep is Jacobi
20 * -- every x_a is read off the PREVIOUS sweep's stationary vectors and only then
21 * do the agents re-solve -- so the agent order is immaterial, and a parallel or
22 * distributed sweep produces the SAME iterates as the serial one.
23 *
24 * HOW FAR THAT SURVIVES FLOATING POINT DEPENDS ON WHO RUNS THE AGENT SOLVE.
25 * `parallel` is BIT-IDENTICAL to `serial`: the same code in the same process,
26 * differing only in an order that does not matter. `cluster` is bit-identical
27 * only when the worker runs the same implementation as the coordinator -- the
28 * wire is exact, since JSON round-trips a double without loss, but the
29 * stationary vector comes back from the WORKER's solve, so a C++ coordinator
30 * driving a Java ag-worker agrees to a few ulp rather than bit for bit. That is
31 * the ordinary cross-codebase difference, not a protocol defect.
32 *
33 * ARITHMETIC. `parallel` carries any arithmetic the agent solve carries, because
34 * it moves no numbers between representations. `cluster` is DOUBLE ONLY and
35 * refuses by name otherwise: the wire is JSON, so an exact rational or a
36 * 200-digit float would have to be rounded to send, and silently answering a
37 * high-precision request at double precision is worse than refusing it.
38 */
39
40#include <algorithm>
41#include <cstddef>
42#include <string>
43#include <thread>
44#include <vector>
45
46#include "line/num/number.h"
49#include "line/util/error.h"
50#include "line/util/matrix.h"
51
52namespace line {
53namespace ag {
54
55/**
56 * Evaluate every agent of one sweep.
57 *
58 * @param n number of agents
59 * @param gen k -> agent k's generator at the current reversed rates
60 * @param sol (Q_k, k) -> agent k's stationary vector
61 * @param Q,pi per-agent output slots, resized by the caller
62 */
63template <class T, class Gen, class Sol>
64void ag_sweep_serial(std::size_t n, Gen gen, Sol sol,
65 std::vector<Matrix<T>>& Q, std::vector<std::vector<T>>& pi) {
66 for (std::size_t k = 0; k < n; ++k) {
67 Q[k] = gen(k);
68 pi[k] = sol(Q[k], k);
69 }
70}
71
72/**
73 * The same sweep over a thread pool. Each task owns one agent and writes only
74 * its own slot, so no synchronisation beyond the join is needed and the result
75 * cannot depend on the interleaving.
76 */
77template <class T, class Gen, class Sol>
78void ag_sweep_parallel(std::size_t n, unsigned nworkers, Gen gen, Sol sol,
79 std::vector<Matrix<T>>& Q, std::vector<std::vector<T>>& pi) {
80 unsigned hw = nworkers > 0 ? nworkers : std::thread::hardware_concurrency();
81 if (hw == 0) hw = 1;
82 const std::size_t nthreads = std::min<std::size_t>(hw, n == 0 ? 1 : n);
83 if (nthreads <= 1) {
84 ag_sweep_serial<T>(n, gen, sol, Q, pi);
85 return;
86 }
87
88 std::vector<std::thread> pool;
89 pool.reserve(nthreads);
90 // A STRIDED partition, not a contiguous one: agent cost varies with the
91 // agent's state-space size, and neighbouring agents are the same station's
92 // classes and so are similarly sized. Striding mixes big and small across
93 // the threads instead of loading one thread with a station's whole block.
94 for (std::size_t t = 0; t < nthreads; ++t) {
95 pool.emplace_back([&, t]() {
96 for (std::size_t k = t; k < n; k += nthreads) {
97 Q[k] = gen(k);
98 pi[k] = sol(Q[k], k);
99 }
100 });
101 }
102 for (std::size_t t = 0; t < pool.size(); ++t) pool[t].join();
103}
104
105/**
106 * The same sweep with the agents partitioned over remote ag-worker processes.
107 *
108 * The generator is rebuilt here in any case -- the metrics stage reads it, and
109 * assembling it is O(N^2) against the O(N^3) solve -- so only the stationary
110 * vector crosses the wire back. An unreachable, slow or broken worker is not
111 * fatal: its agents fall through to @p sol on this process.
112 */
113template <class T, class Gen, class Sol, class Payload>
114void ag_sweep_cluster(std::size_t n, AgWorkerPool& workers, const std::vector<T>& x,
115 Gen gen, Sol sol, Payload payload,
116 std::vector<Matrix<T>>& Q, std::vector<std::vector<T>>& pi) {
117 if constexpr (!std::is_same<T, double>::value) {
118 (void)workers; (void)x; (void)payload;
119 throw UnsupportedError(
120 "ag: the 'cluster' execution backend is double only, because the ag-worker "
121 "protocol is JSON and an exact or high-precision value would have to be rounded "
122 "to send it; rerun with --arith double, or use exec 'serial' or 'parallel'");
123 } else {
124 workers.ensure_assigned(n, payload);
125
126 for (std::size_t k = 0; k < n; ++k) Q[k] = gen(k);
127
128 std::vector<bool> pending(n, true);
129 std::vector<double> xd(x.size());
130 for (std::size_t c = 0; c < x.size(); ++c) xd[c] = x[c];
131
132 workers.sweep(xd, pi, pending);
133
134 for (std::size_t k = 0; k < n; ++k) {
135 if (pending[k]) pi[k] = sol(Q[k], k);
136 }
137 }
138}
139
140} // namespace ag
141} // namespace line
142
143#endif // LINE_SOLVERS_AG_AG_EXEC_H
Options of the agent-based (RCAT) solver.
Coordinator-side connections to the ag-worker processes.
UnsupportedError(const std::string &what)
Definition error.h:51
The connections to every configured worker, plus the agent partition.
void ensure_assigned(std::size_t num_agents, Payload payload)
Connect and ship the static half of each owned agent, once.
void sweep(const std::vector< double > &x, std::vector< std::vector< double > > &pi, std::vector< bool > &pending)
One sweep.
The exception types the port throws.
Dense matrix and non-owning view.
void ag_sweep_cluster(std::size_t n, AgWorkerPool &workers, const std::vector< T > &x, Gen gen, Sol sol, Payload payload, std::vector< Matrix< T > > &Q, std::vector< std::vector< T > > &pi)
The same sweep with the agents partitioned over remote ag-worker processes.
Definition ag_exec.h:114
void ag_sweep_serial(std::size_t n, Gen gen, Sol sol, std::vector< Matrix< T > > &Q, std::vector< std::vector< T > > &pi)
Evaluate every agent of one sweep.
Definition ag_exec.h:64
void ag_sweep_parallel(std::size_t n, unsigned nworkers, Gen gen, Sol sol, std::vector< Matrix< T > > &Q, std::vector< std::vector< T > > &pi)
The same sweep over a thread pool.
Definition ag_exec.h:78
Number-type abstraction for the templated API port.