LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_nc_cache.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_CACHE_H
6#define LINE_SOLVERS_NC_SOLVER_NC_CACHE_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `solver_nc_cache_analyzer.m`: the NON-REENTRANT cache, a model that
12 * is exactly a Source, a Cache and a Sink.
13 *
14 * WHAT THE ANALYZER ACTUALLY COMPUTES. There is no queueing here at all. Each
15 * class reads item k with probability `pread(v,k)`, the cache holds `itemcap`
16 * items per list, and the question is only which items are resident. The answer
17 * is a per-item occupancy `pij` -- column 0 the miss probability, column 1+j the
18 * probability that the item sits on list j -- from which the miss RATE follows
19 * by weighting with the read rates. The throughput table is then just the
20 * source rate split between each class's hit and miss classes.
21 *
22 * THREE ALGORITHMS, AND ONLY ONE OF THEM IS EXACT.
23 *
24 * exact `cache_prob_erec`, the exact recursion. REFUSED for any
25 * replacement policy outside the exchangeable (product-form)
26 * family: RR and FIFO have a product form, LRU / h-LRU / q-LRU /
27 * CLIMB do not, and the recursion would silently return the
28 * exchangeable answer for them.
29 * sampling `cache_miss_is` / `cache_prob_is`, importance sampling.
30 * default the SPM saddle point, which `spm` and `rayint` also name. With
31 * per-item storage costs it is `cache_spm_size`, the size-tilted
32 * expansion, reported as `spm.size`; without them it is
33 * `cache_miss_spm` / `cache_prob_spm`, reported as `spm`.
34 *
35 * WHY THE PER-LIST BREAKDOWN IS NaN OUTSIDE THE EXACT BRANCH, which is a
36 * deliberate refusal to report a number rather than an omission. Only the exact
37 * recursion produces a miss column and per-list columns from ONE consistent
38 * solution, so that the per-list rows sum to the aggregate hit. The approximate
39 * algorithms derive the miss and the per-list columns from different expansions
40 * and their breakdown does not form a distribution for more than one list.
41 *
42 * THE PER-ITEM TABLE IS ALWAYS TAKEN FROM THE EXACT RECURSION, even in the
43 * approximate branches: the cache is product-form, so the exact per-item table
44 * is available regardless of how the aggregate miss rate was obtained. It is
45 * skipped above 10 items, where the recursion stops being tractable.
46 */
47
48#include <algorithm>
49#include <cmath>
50#include <cstddef>
51#include <limits>
52#include <string>
53#include <vector>
54
56#include <sstream>
57
68#include "line/util/error.h"
69#include "line/util/matrix.h"
70
71namespace line {
72namespace nc {
73
74/** What the cache analyzer returns beyond the usual metric table. */
75template <class T>
78 Matrix<T> pij; ///< (n x h+1) per-item occupancy, column 0 = miss
79 Matrix<T> itemprob; ///< (n x h+1) the same from the EXACT recursion, NaN above 10 items
80 Matrix<T> hitproblist; ///< (u x h) access-weighted per-list hit probability, NaN if not exact
81 std::vector<T> missrate; ///< (u) per-class miss rate
82 /**
83 * (u) per-class miss and hit PROBABILITIES, `missrate` divided by the read
84 * class's arrival rate and its complement.
85 *
86 * Carried beside the rate because `getAvgCacheTable` reports a probability
87 * and the two differ by a factor no consumer downstream can recover: the
88 * arrival rate is the Source's and is gone by the time the table is built.
89 * NaN for a class with no read stream, which is not a hit ratio of zero.
90 */
91 std::vector<T> missprob, hitprob;
92 std::vector<T> listcost; ///< (h) mean storage cost held by each list, EMPTY without item sizes
93 /**
94 * Storage cost caps that block a promotion path, so that the exact
95 * recursion normalizes over MORE states than the cache can reach. The port
96 * has no warning channel, so this is a FLAG, like `SjnResult::capped`:
97 * a non-empty vector means cross-check the answer with SolverLDES.
98 */
99 std::vector<cache::CacheBlockedPair> costcap_blocked;
100 /** True when the requested method had no cost-capped counterpart and was switched. */
102};
103
104/**
105 * Port of `solver_nc_cache_analyzer.m`.
106 *
107 * @param sn the refreshed struct; must be a Source-Cache-Sink model
108 * @param opt solver controls; `method` selects exact / sampling / spm
109 */
110template <class T>
112 const NcSolverOptions& opt) {
114 if constexpr (!num_traits<T>::has_transcendental) {
115 (void)sn;
116 (void)opt;
117 throw UnsupportedError(
118 "solver_nc_cache_analyzer: the cache occupancy is a normalizing constant formed in "
119 "logarithms and needs transcendental arithmetic");
120 } else {
121 const T zero = num_traits<T>::from_int(0);
122 const T one = num_traits<T>::from_int(1);
123 const double dnan = std::numeric_limits<double>::quiet_NaN();
124 const std::size_t K = sn.nclasses;
125
126 std::size_t cacheNode = 0, sourceStation = 0;
127 for (std::size_t i = 0; i < sn.nodes.size(); ++i) {
128 if (sn.nodes[i].nodetype == qn::NodeType::Cache) cacheNode = i + 1;
129 if (sn.nodes[i].nodetype == qn::NodeType::Source)
130 sourceStation = sn.nodes[i].station;
131 }
132 if (cacheNode == 0) throw UnsupportedError("solver_nc_cache_analyzer: no Cache node");
133 if (sourceStation == 0) throw UnsupportedError("solver_nc_cache_analyzer: no Source node");
134 const auto itp = sn.nodeparam.find(cacheNode);
135 if (itp == sn.nodeparam.end())
136 throw UnsupportedError("solver_nc_cache_analyzer: the Cache node carries no parameters");
137 const qn::CacheParam<T>& ch = itp->second;
138
139 std::vector<T> sourceRate(K, zero);
140 for (std::size_t r = 0; r < K; ++r)
141 if (!sn.disabled[sourceStation - 1][r]) sourceRate[r] = sn.rates(sourceStation - 1, r);
142
143 const std::vector<int>& m = ch.itemcap;
144 const std::size_t n = ch.nitems;
145 // `n < m + 2` in the reference, where `m` is the CAPACITY VECTOR, not
146 // the list count -- and MATLAB's `if` on a vector requires every entry,
147 // so the gate fires only when the item count is short for EVERY list.
148 // Comparing against the number of lists instead lets a cache with more
149 // capacity than items through, where the recursion has no headroom.
150 {
151 bool shortForAll = !m.empty();
152 for (int cap : m)
153 if (static_cast<long>(n) >= static_cast<long>(cap) + 2) shortForAll = false;
154 if (shortForAll)
155 throw UnsupportedError(
156 "solver_nc_cache_analyzer: NC requires the number of items to exceed the "
157 "cache capacity at least by 2; this cache holds " + std::to_string(n) +
158 " items with capacity " + std::to_string(m[0]));
159 }
160 const std::size_t h = m.size();
161 const std::size_t u = K;
162
163 // lambda[v](k,l): the rate at which class v requests item k while it
164 // sits at node l. The reference fills every list column with the same
165 // rate -- the read rate does not depend on where the item currently is.
166 std::vector<Matrix<T>> lambda(u, Matrix<T>(n, h + 1, zero));
167 for (std::size_t v = 0; v < u; ++v)
168 if (!ch.pread[v].empty())
169 for (std::size_t k = 0; k < n && k < ch.pread[v].size(); ++k)
170 for (std::size_t l = 0; l <= h; ++l)
171 lambda[v](k, l) = T(sourceRate[v] * ch.pread[v][k]);
172
173 // The access-cost matrices. Absent, the reference installs the DEFAULT
174 // LINEAR routing: an item moves from list l to list l+1 on a hit, and
175 // the last list is absorbing.
176 std::vector<std::vector<Matrix<T>>> R = ch.accost;
177 if (R.empty()) {
178 R.assign(u, std::vector<Matrix<T>>(n, Matrix<T>(h + 1, h + 1, zero)));
179 for (std::size_t v = 0; v < u; ++v)
180 for (std::size_t k = 0; k < n; ++k) {
181 Matrix<T> Rm(h + 1, h + 1, zero);
182 for (std::size_t l = 0; l < h; ++l) Rm(l, l + 1) = one;
183 Rm(h, h) = one;
184 R[v][k] = Rm;
185 }
186 }
187
189
190 // lambda(:,:,1) as the (u x n) matrix the miss routines want.
191 Matrix<T> lam1(u, n, zero);
192 for (std::size_t v = 0; v < u; ++v)
193 for (std::size_t k = 0; k < n; ++k) lam1(v, k) = lambda[v](k, 0);
194
195 // Per-item storage costs and per-list cost caps (ton21cache Sec. IX).
196 const std::vector<int>& sigma = ch.itemsize;
197 const std::vector<int>& costcap = ch.costcap;
198 if (!costcap.empty()) {
199 if (sigma.empty())
200 throw InputError(
201 "solver_nc_cache_analyzer: storage cost caps require per-item sizes");
202 if (sigma.size() != n)
203 throw InputError(
204 "solver_nc_cache_analyzer: the item size vector must have one entry per item");
205 if (costcap.size() != h)
206 throw InputError(
207 "solver_nc_cache_analyzer: the cost cap vector must have one entry per cache "
208 "list");
209 out.costcap_blocked =
210 cache::cache_cost_pathcheck(gr.gamma, sigma, costcap, gr.parent);
211 if (!out.costcap_blocked.empty()) {
212 // Carried on the solution's warning channel, not just as a flag:
213 // the reference warns here, and a flag no caller reads says nothing
214 std::ostringstream w;
215 w << "storage cost caps block the promotion path of item "
216 << (out.costcap_blocked[0].item + 1) << " into list "
217 << (out.costcap_blocked[0].list + 1) << " at list "
218 << (out.costcap_blocked[0].blocking_list + 1) << " (and "
219 << (out.costcap_blocked.size() - 1)
220 << " further pairs). The exact recursion normalizes over all size-feasible "
221 "states, which is then a strict superset of the states the cache can reach; "
222 "cross-check with SolverLDES.";
223 out.sol.warning = w.str();
224 }
225 }
226
227 std::string cacheMethod = opt.method;
228 // 'rayint' is an alias of 'spm': on a cache both name the SPM saddle point,
229 // and the method name stays live for solver_nc_retrieval_analyzer's delayed-hit
230 // expansion.
231 if (cacheMethod == "rayint" || cacheMethod == "spm") cacheMethod = "default";
232 // The SPM family serves its size-tilted form (cache_spm_size) once the items
233 // carry storage costs. The saddle escapes to infinity at sum(m) = n, so the
234 // size-free saddle point takes over there rather than the exact recursion,
235 // which would refuse every replacement policy outside RR/FIFO.
236 long msum = 0;
237 for (std::size_t j = 0; j < h; ++j) msum += m[j];
238 const bool useSpmSize =
239 cacheMethod == "default" && !sigma.empty() && msum < static_cast<long>(n);
240 if (!costcap.empty() && !useSpmSize && cacheMethod != "exact" &&
241 cacheMethod != "sampling") {
242 // The size-free SPM and the mean-field methods have no cost-capped
243 // counterpart: the k - sigma_i e_j argument couples item sizes into the
244 // recursion graph, which only cache_spm_size and the exact path carry.
245 double lattice = static_cast<double>(n);
246 for (std::size_t j = 0; j < h; ++j)
247 lattice *= static_cast<double>(m[j] + 1) * static_cast<double>(costcap[j] + 1);
248 cacheMethod = (lattice <= 1e6) ? "exact" : "sampling";
249 out.costcap_method_switched = true;
250 std::ostringstream w;
251 w << "method '" << opt.method << "' does not support storage cost caps; using '"
252 << cacheMethod << "' instead.";
253 out.sol.warning = out.sol.warning.empty() ? w.str() : out.sol.warning + " " + w.str();
254 }
255
256 std::vector<T> missRate(u, zero);
257 bool exact = false;
258 std::string method;
259 if (cacheMethod == "exact") {
260 // cache_prob_erec is exact only for the exchangeable family.
263 throw UnsupportedError(
264 "solver_nc_cache_analyzer: NC does not support the exact solution of this "
265 "cache replacement policy -- only RR and FIFO are exchangeable, and a "
266 "recency-based policy (LRU, h-LRU, q-LRU, CLIMB) would silently receive the "
267 "exchangeable answer. Use the default (approximate) method or SolverCTMC");
268 out.pij = cache::cache_prob_erec(gr.gamma, m, sigma, costcap);
269 for (std::size_t v = 0; v < u; ++v) {
270 T acc = zero;
271 for (std::size_t k = 0; k < n; ++k) acc += T(lambda[v](k, 0) * out.pij(k, 0));
272 missRate[v] = acc;
273 }
274 exact = true;
275 method = "exact";
276 } else if (useSpmSize) {
277 // Size-tilted SPM: a 2h Newton solve whose cost does not grow with the
278 // (m,k) lattice the exact recursion walks. O(1/n), so it wants room
279 // between the occupancies and n.
280 std::vector<int> raycap = costcap;
281 if (raycap.empty()) {
282 // Sizes but no caps: cap each list at the dearest load it can hold,
283 // which is exactly slack, so the cost coordinate leaves the saddle
284 // and the expansion degenerates to the size-free one.
285 std::vector<int> srt = sigma;
286 std::sort(srt.begin(), srt.end(), std::greater<int>());
287 raycap.assign(h, 0);
288 for (std::size_t j = 0; j < h; ++j) {
289 int top = 0;
290 for (int a = 0; a < m[j]; ++a) top += srt[static_cast<std::size_t>(a)];
291 raycap[j] = top;
292 }
293 }
295 cache::cache_spm_size<T>(gr.gamma, m, sigma, raycap);
296 out.pij = ray.pij;
297 for (std::size_t v = 0; v < u; ++v) {
298 T acc = zero;
299 for (std::size_t k = 0; k < n; ++k) acc += T(lambda[v](k, 0) * out.pij(k, 0));
300 missRate[v] = acc;
301 }
302 method = "spm.size";
303 } else if (cacheMethod == "sampling") {
305 cache::cache_miss_is(gr.gamma, m, lam1, opt.samples, opt.seed, sigma, costcap);
306 missRate = mi.MU;
307 out.pij = cache::cache_prob_is(gr.gamma, m, opt.samples, opt.seed, sigma, costcap);
308 method = "sampling";
309 } else {
310 // Size-free SPM, the default/spm/rayint branch with no item sizes.
312 missRate = ms.MU;
313 out.pij = cache::cache_prob_spm(gr.gamma, m);
314 method = "spm";
315 }
316 out.missrate = missRate;
317 // The probabilities the cache table reports, formed where the arrival
318 // rate is still in scope.
319 out.missprob.assign(u, num_traits<T>::from_double(dnan));
320 out.hitprob.assign(u, num_traits<T>::from_double(dnan));
321 for (std::size_t v = 0; v < u; ++v) {
322 if (v >= sourceRate.size()) break;
323 const double lam = num_traits<T>::to_double(sourceRate[v]);
324 if (!(lam > 0.0)) continue;
325 out.missprob[v] = T(missRate[v] / sourceRate[v]);
326 out.hitprob[v] = T(num_traits<T>::from_int(1) - out.missprob[v]);
327 }
328
329 // The metric table. There is no queueing, so only the throughput row of
330 // the Source and the per-class hit/miss split carry anything.
331 const std::size_t M = sn.nstations;
332 out.sol.sol.Q = Matrix<T>(M, K, zero);
333 out.sol.sol.U = Matrix<T>(M, K, zero);
334 out.sol.sol.R = Matrix<T>(M, K, zero);
335 out.sol.sol.Tp = Matrix<T>(M, K, zero);
336 out.sol.sol.C.assign(K, zero);
337 out.sol.sol.X.assign(K, zero);
338 for (std::size_t r = 0; r < K; ++r) out.sol.sol.Tp(sourceStation - 1, r) = sourceRate[r];
339 for (std::size_t r = 0; r < K; ++r) {
340 if (r >= ch.hitclass.size() || r >= ch.missclass.size()) continue;
341 const std::size_t hc = ch.hitclass[r], mc = ch.missclass[r];
342 if (hc == 0 || mc == 0) continue;
343 out.sol.sol.X[mc - 1] = T(out.sol.sol.X[mc - 1] + missRate[r]);
344 out.sol.sol.X[hc - 1] = T(out.sol.sol.X[hc - 1] + (sourceRate[r] - missRate[r]));
345 }
346 out.sol.sol.lG = 0.0;
347 out.sol.sol.iter = 1;
348 out.sol.sol.method = method;
349 out.sol.actualmethod = method;
350
351 // Per-list hit probability, access-weighted. Reported ONLY from the
352 // exact branch, where the per-list columns and the miss column come
353 // from one solution; see the header.
355 if (exact)
356 for (std::size_t v = 0; v < u; ++v) {
357 if (ch.pread[v].empty()) continue;
358 for (std::size_t l = 0; l < h; ++l) {
359 T acc = zero;
360 for (std::size_t k = 0; k < n && k < ch.pread[v].size(); ++k)
361 acc += T(ch.pread[v][k] * out.pij(k, l + 1));
362 out.hitproblist(v, l) = acc;
363 }
364 }
365
366 // Per-item occupancy. Always the EXACT recursion, since the cache is
367 // product-form; skipped above 10 items where it stops being tractable.
368 if (n > 10) {
369 out.itemprob = Matrix<T>(n, h + 1, num_traits<T>::from_double(dnan));
370 } else if (exact) {
371 out.itemprob = out.pij;
372 } else {
373 out.itemprob = cache::cache_prob_erec(gr.gamma, m, sigma, costcap);
374 }
375
376 // Mean storage cost held by each list, K_j = sum_i sigma_i pi_ij. Kept on
377 // the inner NcSolution too, since the runner returns only that and the
378 // cost would otherwise never leave this function.
379 if (!sigma.empty() && out.pij.rows() == n && out.pij.cols() == h + 1) {
380 out.listcost = cache::cache_cost(gr.gamma, m, sigma, costcap, out.pij);
381 out.sol.listcost = out.listcost;
382 }
383 return out;
384 }
385}
386
387/** True when the model is exactly a Source, a Cache and a Sink. */
388template <class T>
390 if (sn.nodes.size() != 3) return false;
391 int src = 0, ca = 0, snk = 0;
392 for (const qn::NodeDef& nd : sn.nodes) {
393 if (nd.nodetype == qn::NodeType::Source) ++src;
394 else if (nd.nodetype == qn::NodeType::Cache) ++ca;
395 else if (nd.nodetype == qn::NodeType::Sink) ++snk;
396 }
397 if (!(src == 1 && ca == 1 && snk == 1)) return false;
398 for (const qn::JobClass& c : sn.classes)
399 if (std::isfinite(c.population) && c.population > 0.0) return false;
400 return true;
401}
402
403} // namespace nc
404} // namespace line
405
406#endif // LINE_SOLVERS_NC_SOLVER_NC_CACHE_H
Mean per-list storage cost of a cache with item sizes, and the screen for promotion paths that storag...
Access factors of a tree-structured multi-list cache.
Cache miss rates from the importance-sampling hit probabilities.
Saddle-point approximation of the cache miss rates.
Exact per-item hit and miss probabilities of a multi-list cache.
Importance-sampling estimate of the cache hit-probability distribution.
Saddle-point approximation of the per-item cache hit probabilities.
Ray (WKB) asymptotic expansion of the cost-capped cache normalizing constant.
InputError(const std::string &what)
Definition error.h:39
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
The exception types the port throws.
Dense matrix and non-owning view.
std::vector< CacheBlockedPair > cache_cost_pathcheck(const Matrix< T > &gamma, const std::vector< int > &sigma, const std::vector< int > &k, const std::vector< int > &parent)
Definition cache_cost.h:95
std::vector< T > cache_cost(const Matrix< T > &gamma, const std::vector< int > &m, const std::vector< int > &sigma, const std::vector< int > &k, const Matrix< T > &pij)
Mean per-list storage cost of a cache with item sizes, and the screen for promotion paths that storag...
Definition cache_cost.h:63
CacheGammaResult< T > cache_gamma_lp(const std::vector< Matrix< T > > &lambda, const std::vector< std::vector< Matrix< T > > > &R)
Access factors of a tree-structured multi-list cache.
CacheMissIsResult< T > cache_miss_is(const Matrix< T > &gamma, const std::vector< int > &m, const Matrix< T > &lambda, std::size_t samples, std::uint64_t seed, const std::vector< int > &sigma, const std::vector< int > &cap)
Cache miss rates from the importance-sampling hit probabilities.
Matrix< T > cache_prob_erec(const Matrix< T > &gamma, const std::vector< int > &m, const std::vector< int > &sigma, const std::vector< int > &k)
Per-item hit and miss probabilities under per-list storage cost caps, pi_ij = m_j gamma(i,...
Matrix< T > cache_prob_is(const Matrix< T > &gamma, const std::vector< int > &m, std::size_t samples, std::uint64_t seed, const std::vector< int > &sigma, const std::vector< int > &k)
Importance-sampling estimate of the cache hit-probability distribution.
CacheSpmSizeResult< T > cache_spm_size(const Matrix< T > &gamma, const std::vector< int > &m, const std::vector< int > &sigma, const std::vector< int > &k, CacheCostMode mode=CacheCostMode::AtMost)
Ray (WKB) asymptotic expansion of the cost-capped cache normalizing constant.
CacheMissSpmResult< T > cache_miss_spm(const Matrix< T > &gamma, const std::vector< int > &m, const Matrix< T > &lambda)
Saddle-point approximation of the cache miss rates.
Matrix< T > cache_prob_spm(const Matrix< T > &gamma, const std::vector< int > &m)
Saddle-point approximation of the per-item cache hit probabilities.
@ FIFO
first in, first out
Definition lang_types.h:380
NcCacheSolution< T > solver_nc_cache_analyzer(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt)
Port of solver_nc_cache_analyzer.m.
bool nc_is_noreentrant_cache(const qn::NetworkStruct< T > &sn)
True when the model is exactly a Source, a Cache and a Sink.
Controls and result shape shared by the normalizing-constant analyzers.
A queueing network and its refreshed NetworkStruct.
Port of solver_nc.m: the load-INDEPENDENT normalizing-constant analyzer.
Return value of cache_gamma_lp, mirroring [gamma,u,n,h].
std::vector< int > parent
Parent list of each list, 0-based, -1 for lists rooted in the miss list.
Matrix< T > gamma
(n x h) access factors
std::vector< T > MU
(u) per-user miss rate; empty when no lambda given
Return value of cache_miss_spm, mirroring [M,MU,MI,pi0,lE].
std::vector< T > MU
(u) per-user miss rate
Outcome of the expansion.
Matrix< T > pij
Occupancy pi, n x (h+1), column 0 the miss probability.
What the cache analyzer returns beyond the usual metric table.
Matrix< T > hitproblist
(u x h) access-weighted per-list hit probability, NaN if not exact
Matrix< T > pij
(n x h+1) per-item occupancy, column 0 = miss
std::vector< T > missrate
(u) per-class miss rate
std::vector< cache::CacheBlockedPair > costcap_blocked
Storage cost caps that block a promotion path, so that the exact recursion normalizes over MORE state...
std::vector< T > missprob
(u) per-class miss and hit PROBABILITIES, missrate divided by the read class's arrival rate and its c...
Matrix< T > itemprob
(n x h+1) the same from the EXACT recursion, NaN above 10 items
bool costcap_method_switched
True when the requested method had no cost-capped counterpart and was switched.
std::vector< T > listcost
(h) mean storage cost held by each list, EMPTY without item sizes
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< int > itemsize
Per-item storage cost (size) and per-list cap on the total cost of the resident items (ton21cache Sec...
std::vector< int > costcap
std::vector< std::vector< Matrix< T > > > accost
(u) x (n) of (h+1)x(h+1), or empty
std::vector< int > itemcap
std::vector< std::size_t > missclass
std::vector< std::size_t > hitclass
lang::ReplacementStrategy replacestrat
std::vector< std::vector< T > > pread
(u) x (n), empty row = NaN
One job class of the network.
double population
infinite for an open class
A node of the network.