LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_nc_retrieval.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_NC_SOLVER_NC_RETRIEVAL_H
6#define LINE_SOLVERS_NC_SOLVER_NC_RETRIEVAL_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `solver_nc_retrieval_analyzer.m`: the OPEN delayed-hit
12 * (retrieval-system) cache.
13 *
14 * WHAT A DELAYED HIT IS, since the three-way split is the whole point. A miss
15 * does not simply fail: it starts a FETCH that circulates the item through the
16 * retrieval stations and back into the cache. A read arriving while that fetch
17 * is still in flight is neither a hit (the item is not resident) nor an
18 * ordinary miss (no second fetch is started) -- it is a DELAYED HIT, and it
19 * waits for the fetch already running. So
20 *
21 * true hit + delayed hit + miss = 1
22 *
23 * and only the MISS fraction starts new work. That is why the throughput split
24 * below sends hits AND delayed hits to the hit class, and misses alone to the
25 * miss class: the delayed hit is served by a fetch the miss already paid for.
26 *
27 * EXACT, NOT A FIXED POINT. SolverMVA solves this shape with `retrieval_fpi`,
28 * iterating to a tolerance. SolverNC uses the product-form recurrences instead:
29 * `retrieval_nc` for the normalizing constant and `retrieval_metrics` for the
30 * three ratios, both exact. The reference reports `method = 'exact'` on that
31 * basis and leaves LATENCY to SolverMVA, returning NaN -- the latency needs
32 * `retrieval_fpi_latency`, which is a different algorithm and not this one's.
33 *
34 * THE RETRIEVAL STATIONS ARE READ OFF phi, NOT SOLVED. `pdh(s,i)` is the mean
35 * number of copies of item i being fetched at station s, so summing over items
36 * gives the station occupancy directly; the throughput comes from the per-item
37 * fetch rate times the visit ratios of the fetch routing, and the response time
38 * from Little's law. No queueing solve is involved.
39 */
40
41#include <cmath>
42#include <cstddef>
43#include <limits>
44#include <string>
45#include <vector>
46
54#include "line/util/error.h"
55#include "line/util/linalg.h"
56#include "line/util/matrix.h"
57
58namespace line {
59namespace nc {
60
61/** What the delayed-hit analyzer returns beyond the metric table. */
62template <class T>
65 std::vector<T> hitprob; ///< (K) TRUE hit fraction, NaN off the read class
66 std::vector<T> missprob; ///< (K)
67 std::vector<T> delayedprob; ///< (K) delayed-hit fraction
68 std::vector<T> latency; ///< (K) NaN: the latency belongs to SolverMVA
69 Matrix<T> hitproblist; ///< (K x h) per-list hit fraction
70 Matrix<T> itemprob; ///< (n x h+1) column 0 = miss, 1.. = per list
71};
72
73/**
74 * Port of `solver_nc_retrieval_analyzer.m`.
75 *
76 * @param sn the refreshed struct; the Cache must carry a retrieval system
77 * @param opt solver controls
78 */
79template <class T>
81 const NcSolverOptions& opt) {
83 if constexpr (!num_traits<T>::has_transcendental) {
84 (void)sn;
85 (void)opt;
86 throw UnsupportedError(
87 "solver_nc_retrieval_analyzer: the delayed-hit recurrences form a normalizing "
88 "constant in logarithms and need transcendental arithmetic");
89 } else {
90 const T zero = num_traits<T>::from_int(0);
91 const T one = num_traits<T>::from_int(1);
92 const T nanT = num_traits<T>::from_double(std::numeric_limits<double>::quiet_NaN());
93 const std::size_t K = sn.nclasses, M = sn.nstations;
94
96 const std::size_t n = in.lambda.size();
97 const std::size_t h = in.m.size();
98 const std::size_t S = in.queue_nodes.size();
99 std::size_t r = 0; // the PS-like stations, which carry their own eta column
100 for (const retrieval::RetrievalStationPH<T>& st : in.station)
102
103 // The ray expansion needs the delayed-hit constant to factorize. It does,
104 // EXACTLY, when every fetch station is infinite-server: dividing the
105 // retrieval_nc recurrence by prod_k D_k with D_k = 1 + lambda_k eta_{0,k}
106 // collapses it onto cache_erec with theta_{k,j} = gamma_{k,j}/D_k, so the
107 // delayed-hit cache IS a plain cache with fetch-inflated access factors.
108 // A queueing (PS) fetch station breaks this: the (v_s+1) multiplicity ties
109 // E(0,m) to the whole moment tower E(1_s,m), E(2_s,m), ..., and replacing it
110 // by the retrieval_fpi mean field overestimates E by 13%/140%/830% at
111 // n=6/8/10 (measured), growing with n. Refuse rather than return a
112 // confident wrong number.
113 bool useray = (opt.method == "rayint" || opt.method == "ray");
114 if (useray) {
115 std::string reason;
116 bool has_ps = false;
117 for (std::size_t i = 0; i < n && !has_ps; ++i)
118 for (std::size_t sc = 1; sc <= r; ++sc)
119 if (num_traits<T>::to_double(in.eta(i, sc)) != 0.0) { has_ps = true; break; }
120 long msum_chk = 0;
121 for (std::size_t j = 0; j < h; ++j) msum_chk += in.m[j];
122 if (has_ps)
123 reason = "the retrieval system has a queueing (non infinite-server) fetch station";
124 else if (msum_chk >= static_cast<long>(n))
125 reason = "the cache is full (sum(m) >= n), where the saddle point escapes to infinity";
126 if (!reason.empty()) {
127 const std::string w = "SolverNC: method 'rayint' does not apply because " + reason +
128 "; falling back to the exact recurrences.";
129 out.sol.warning = out.sol.warning.empty() ? w : out.sol.warning + " " + w;
130 useray = false;
131 }
132 }
133
135 if (useray) {
136 // --- ray (WKB) approximation, infinite-server fetch ---
137 std::vector<T> D(n);
138 Matrix<T> theta(n, h);
139 for (std::size_t i = 0; i < n; ++i) {
140 D[i] = one + in.lambda[i] * in.eta(i, 0);
141 for (std::size_t j = 0; j < h; ++j) theta(i, j) = in.gamma(i, j) / D[i];
142 }
145 double logD = 0.0;
146 for (std::size_t i = 0; i < n; ++i) logD += std::log(num_traits<T>::to_double(D[i]));
147 out.sol.sol.lG = logD + num_traits<T>::to_double(ray.log_e);
148
149 // Same saddle as the constant, so the ratios are consistent with lG:
150 // pi_{i,j} = theta_{i,j} xi_j / (1 + sum_l theta_{i,l} xi_l), and the
151 // out-of-cache mass 1 - sum_j pi_{i,j} splits between a true miss (weight 1)
152 // and an outstanding fetch (weight lambda_i eta_{0,i}) in proportion 1:D_i-1.
153 mt.pmiss.assign(n, zero);
154 mt.phit = Matrix<T>(h, n, zero);
155 mt.pdh = Matrix<T>(1, n, zero);
156 for (std::size_t i = 0; i < n; ++i) {
157 T den = one;
158 for (std::size_t j = 0; j < h; ++j) den += theta(i, j) * ray.xi[j];
159 T pihit = zero;
160 for (std::size_t j = 0; j < h; ++j) {
161 mt.phit(j, i) = theta(i, j) * ray.xi[j] / den;
162 pihit += mt.phit(j, i);
163 }
164 mt.pmiss[i] = (one - pihit) / D[i];
165 mt.pdh(0, i) = in.lambda[i] * in.eta(i, 0) * mt.pmiss[i];
166 }
167 } else {
168 // Exact normalizing constant E(m) = retrieval_nc(0, m, ...).
169 const T E = retrieval::retrieval_nc(std::vector<int>(r, 0), in.m, in.lambda, in.eta,
170 in.gamma);
171 out.sol.sol.lG = std::log(num_traits<T>::to_double(E));
172
173 mt = retrieval::retrieval_metrics(in.m, in.lambda, in.eta, in.gamma);
174 }
175
176 // Per-item aggregates: hit summed over lists, delayed summed over the
177 // fetch stations.
178 std::vector<T> pih(n, zero), phid(n, zero);
179 for (std::size_t i = 0; i < n; ++i) {
180 for (std::size_t j = 0; j < mt.phit.rows(); ++j) pih[i] += mt.phit(j, i);
181 for (std::size_t s = 0; s < mt.pdh.rows(); ++s) phid[i] += mt.pdh(s, i);
182 }
183
184 // The access-weighted item mixture.
185 T lamtot = zero;
186 for (const T& v : in.lambda) lamtot += v;
187 std::vector<T> w(n, zero);
188 for (std::size_t i = 0; i < n; ++i) w[i] = lamtot > zero ? T(in.lambda[i] / lamtot) : zero;
189
190 T hitAgg = zero, missAgg = zero, delayedAgg = zero;
191 for (std::size_t i = 0; i < n; ++i) {
192 hitAgg += T(w[i] * pih[i]);
193 missAgg += T(w[i] * mt.pmiss[i]);
194 delayedAgg += T(w[i] * phid[i]);
195 }
196
197 const std::size_t rc = in.read_class; // 1-based
198 std::size_t sourceStation = 0;
199 for (const qn::NodeDef& nd : sn.nodes)
200 if (nd.nodetype == qn::NodeType::Source) sourceStation = nd.station;
201 if (sourceStation == 0)
202 throw UnsupportedError(
203 "solver_nc_retrieval_analyzer: the OPEN delayed-hit analyzer needs a Source node; "
204 "a closed integrated model is solved by solver_nc_cacheqn_retrieval_analyzer");
205 std::vector<T> sourceRate(K, zero);
206 for (std::size_t k = 0; k < K; ++k)
207 if (!sn.disabled[sourceStation - 1][k]) sourceRate[k] = sn.rates(sourceStation - 1, k);
208
209 out.sol.sol.Q = Matrix<T>(M, K, zero);
210 out.sol.sol.U = Matrix<T>(M, K, zero);
211 out.sol.sol.R = Matrix<T>(M, K, zero);
212 out.sol.sol.Tp = Matrix<T>(M, K, zero);
213 out.sol.sol.X.assign(K, zero);
214 out.sol.sol.C.assign(K, zero);
215 for (std::size_t k = 0; k < K; ++k) out.sol.sol.Tp(sourceStation - 1, k) = sourceRate[k];
216
217 out.hitprob.assign(K, nanT);
218 out.missprob.assign(K, nanT);
219 out.delayedprob.assign(K, nanT);
220 out.latency.assign(K, nanT);
221 out.hitprob[rc - 1] = hitAgg;
222 out.missprob[rc - 1] = missAgg;
223 out.delayedprob[rc - 1] = delayedAgg;
224
225 out.hitproblist = Matrix<T>(K, h, nanT);
226 for (std::size_t j = 0; j < h && j < mt.phit.rows(); ++j) {
227 T acc = zero;
228 for (std::size_t i = 0; i < n; ++i) acc += T(mt.phit(j, i) * w[i]);
229 out.hitproblist(rc - 1, j) = acc;
230 }
231
232 out.itemprob = Matrix<T>(n, h + 1, zero);
233 for (std::size_t i = 0; i < n; ++i) {
234 out.itemprob(i, 0) = mt.pmiss[i];
235 for (std::size_t j = 0; j < h && j < mt.phit.rows(); ++j)
236 out.itemprob(i, j + 1) = mt.phit(j, i);
237 }
238
239 // A DELAYED HIT IS SERVED BY A FETCH THE MISS ALREADY STARTED, so it
240 // leaves through the HIT class; only the miss fraction starts new work.
241 std::size_t cacheNode = 0;
242 for (std::size_t i = 0; i < sn.nodes.size(); ++i)
243 if (sn.nodes[i].nodetype == qn::NodeType::Cache) cacheNode = i + 1;
244 if (cacheNode == 0) throw UnsupportedError("solver_nc_retrieval_analyzer: no Cache node");
245 const qn::CacheParam<T>& ch = sn.nodeparam.at(cacheNode);
246 if (rc - 1 < ch.hitclass.size() && ch.hitclass[rc - 1] > 0)
247 out.sol.sol.X[ch.hitclass[rc - 1] - 1] =
248 T(sourceRate[rc - 1] * (hitAgg + delayedAgg));
249 if (rc - 1 < ch.missclass.size() && ch.missclass[rc - 1] > 0)
250 out.sol.sol.X[ch.missclass[rc - 1] - 1] = T(sourceRate[rc - 1] * missAgg);
251
252 // The retrieval stations, read off phi rather than solved. `pdh` row 0
253 // is the aggregate IS row and rows 1..r are the PS-like stations in
254 // order, which is why the row index is a running count and not `s`.
255 std::vector<std::size_t> psRow(S, 0);
256 {
257 std::size_t seen = 0;
258 for (std::size_t s = 0; s < S; ++s)
260 psRow[s] = ++seen; // 1-based row in pdh
261 else
262 psRow[s] = 0; // the aggregate IS row
263 }
264 for (std::size_t s = 0; s < S; ++s) {
265 const std::size_t nd = in.queue_nodes[s];
266 const std::size_t ist = sn.nodes[nd - 1].station;
267 if (ist == 0) continue;
268 T phi_s = zero;
269 if (psRow[s] < mt.pdh.rows())
270 for (std::size_t i = 0; i < n; ++i) phi_s += mt.pdh(psRow[s], i);
271 // Fetch throughput: the per-item fetch rate times the visit ratios
272 // of the fetch routing, v = a (I - P)^{-1}.
273 T tput_s = zero;
274 for (std::size_t i = 0; i < n; ++i) {
275 const Matrix<T>& Ri = in.R[i];
276 std::vector<T> a(S, zero);
277 Matrix<T> Pm(S, S, zero);
278 for (std::size_t x = 0; x < S; ++x) {
279 a[x] = Ri(0, x + 1);
280 for (std::size_t y = 0; y < S; ++y) Pm(x, y) = Ri(x + 1, y + 1);
281 }
282 Matrix<T> A(S, S, zero);
283 for (std::size_t x = 0; x < S; ++x) {
284 for (std::size_t y = 0; y < S; ++y) A(y, x) = T(-Pm(x, y));
285 A(x, x) = T(A(x, x) + one);
286 }
287 const std::vector<T> vis = solve(A, a);
288 tput_s += T(sourceRate[rc - 1] * w[i] * mt.pmiss[i] * vis[s]);
289 }
290 out.sol.sol.Q(ist - 1, rc - 1) = phi_s;
291 out.sol.sol.U(ist - 1, rc - 1) = phi_s;
292 out.sol.sol.Tp(ist - 1, rc - 1) = tput_s;
293 if (tput_s > zero) out.sol.sol.R(ist - 1, rc - 1) = T(phi_s / tput_s);
294 }
295
296 out.sol.sol.iter = 1;
297 out.sol.sol.method = useray ? "rayint" : "exact";
298 out.sol.actualmethod = useray ? "rayint" : "exact";
299 return out;
300 }
301}
302
303/** True when the model's Cache carries a delayed-hit retrieval system. */
304template <class T>
306 for (const auto& kv : sn.nodeparam)
307 if (kv.second.retrieval_capacity > 0) return true;
308 return false;
309}
310
311} // namespace nc
312} // namespace line
313
314#endif // LINE_SOLVERS_NC_SOLVER_NC_RETRIEVAL_H
Extract the delayed-hit retrieval-algorithm inputs from a NetworkStruct, a port of matlab/src/api/ret...
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
The exception types the port throws.
Dense linear algebra over the templated number type: products, identity, inverse, and powers.
Dense matrix and non-owning view.
NcRetrievalSolution< T > solver_nc_retrieval_analyzer(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt)
Port of solver_nc_retrieval_analyzer.m.
bool nc_has_retrieval(const qn::NetworkStruct< T > &sn)
True when the model's Cache carries a delayed-hit retrieval system.
RetrievalInputs< T > cache_retrieval_inputs(const qn::NetworkStruct< T > &sn, double lambda_override=-1.0)
Extract the delayed-hit retrieval-algorithm inputs from a NetworkStruct, a port of matlab/src/api/ret...
RetrievalRayintResult< T > retrieval_rayint(const Matrix< T > &gamma, const std::vector< int > &m)
Discrete (saddle) form.
T retrieval_nc(const std::vector< int > &v, const std::vector< int > &m, const std::vector< T > &lambda, const Matrix< T > &eta, const Matrix< T > &gamma)
Exact normalizing constant E(v,m) of a delayed-hit (list-based) cache.
RetrievalMetricsResult< T > retrieval_metrics(const std::vector< int > &m, const std::vector< T > &lambda, const Matrix< T > &eta, const Matrix< T > &gamma)
Exact miss, hit and delayed-hit metrics of a delayed-hit (list-based) cache.
std::vector< T > solve(const Matrix< T > &A, const std::vector< T > &b)
Convenience: solve Ax = b, leaving A and b untouched.
Definition lu.h:158
Controls and result shape shared by the normalizing-constant analyzers.
A queueing network and its refreshed NetworkStruct.
Exact miss, hit and delayed-hit metrics of a delayed-hit (list-based) cache.
Exact normalizing constant E(v,m) of a delayed-hit (list-based) cache.
Ray (WKB) asymptotic expansion of the list-based cache normalizing constant.
Port of solver_nc.m: the load-INDEPENDENT normalizing-constant analyzer.
What the delayed-hit analyzer returns beyond the metric table.
Matrix< T > itemprob
(n x h+1) column 0 = miss, 1.. = per list
std::vector< T > hitprob
(K) TRUE hit fraction, NaN off the read class
Matrix< T > hitproblist
(K x h) per-list hit fraction
std::vector< T > delayedprob
(K) delayed-hit fraction
std::vector< T > latency
(K) NaN: the latency belongs to SolverMVA
The [Q,U,R,T,C,X,lG] of the reference, plus the algorithm that ran.
Definition nc_types.h:113
Controls, defaulting to SolverOptions('NC') in the reference.
Definition nc_types.h:33
std::vector< std::size_t > missclass
std::vector< std::size_t > hitclass
A node of the network.
The [m, lambda, gamma, eta, alpha/T (station), R] the retrieval algorithms read.
std::vector< int > m
(h) list capacities
std::vector< Matrix< T > > R
(n) routing over cache+stations, (S+1)x(S+1)
std::size_t read_class
1-based read (job-in) class
Matrix< T > gamma
(n x h) access factors
std::vector< std::size_t > queue_nodes
1-based retrieval station nodes
std::vector< T > lambda
(n) per-item arrival rates
std::vector< RetrievalStationPH< T > > station
(S) per-station PH service + type
Matrix< T > eta
(n x (r+1)) fetching demands
Mirrors the [pmiss, phit, pdh] return list of the MATLAB function.
Matrix< T > phit
(h x n) hit ratios pi_{i,j}
Matrix< T > pdh
((r+1) x n) delayed-hit probabilities phi_{s,i}, s = 0..r
std::vector< T > pmiss
(n) miss ratios pi_{i,0}
T log_e
Natural logarithm of e, safe for large n.
std::vector< T > xi
Saddle point xi_j, one entry per list (0 for a list of zero capacity).
Phase-type service of every item at one retrieval station.