LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
cache_ttl_lrua.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_CACHE_TTL_LRUA_H
6#define LINE_API_CACHE_TTL_LRUA_H
7
8/**
9 * @file
10 * @ingroup api_cache
11 * TTL (characteristic-time) approximation of an LRU cache whose lists form an
12 * arbitrary access graph.
13 *
14 * Templated port of matlab/src/api/cache/cache_ttl_lrua.m, cross-checked
15 * against jar/src/main/java/jline/api/cache/Cache_ttl_lrua.java.
16 *
17 * Each item moves over the h+1 nodes "not cached" (node 0) and "in list l"
18 * (node l), driven by the access graph R and by exponential timer races: given
19 * characteristic times x_1..x_h, an item in list l is promoted along R with
20 * probability 1 - exp(-lambda_l x_l) and demoted with probability
21 * exp(-lambda_l x_l). The embedded chain over the reachable nodes is solved by
22 * dtmc_solve, the mean holding times are 1/lambda_0 at node 0 and
23 * (1 - exp(-lambda_l x_l))/lambda_l in list l, and the time-stationary
24 * probabilities are the holding-time weighted normalization of the two. The
25 * times are then fixed by sum_k prob(k,l) = m_l, one equation per list.
26 *
27 * HOW THE SYSTEM IS SOLVED: as in cache_t_lrum_map.h and cache_t_hlru.h, the
28 * occupancy of list l is increasing in its own characteristic time, so the
29 * system is h scalar bracketed root problems swept Gauss-Seidel, closed by
30 * bisection. This is deterministic. MATLAB instead calls fsolve from a RANDOM
31 * initial point -- rng(seed,'twister') with a default seed of 23000 and
32 * x = 10*rand(1,h) -- so its answer depends on the seed argument and on the
33 * Optimization Toolbox, and it stops at fsolve's default tolerance (the
34 * reference instance in the tests leaves capacity residuals of ~1.4e-7). The
35 * JAR uses a damped Newton on log(T) with a finite-difference Jacobian, which
36 * is deterministic but still needs a Jacobian and a line search.
37 *
38 * ARITHMETIC: exp and a bisection tolerance, so transcendental arithmetic is
39 * required.
40 *
41 * LATENT CONSTRAINT IN THE REFERENCE: MATLAB reads the demotion probability as
42 * exp(-lambda(1,i,k)*x(k-1)) for every k that is a successor of some node j,
43 * which for k = 1 (the "not cached" node, 0 here) indexes x(0) and is a hard
44 * MATLAB error. An access graph that routes back into the not-cached node is
45 * therefore not expressible; this port raises InputError on it instead of
46 * relying on an index-out-of-range.
47 *
48 * ONE-USER REDUCTION: the MATLAB signature takes a (u x n x h+1) array but its
49 * body reads lambda(1,i,j) only, so every user beyond the first is ignored.
50 * The port takes the (n x h+1) matrix that MATLAB actually uses, which makes
51 * the reduction explicit rather than silent. The caller in
52 * solver_mva_cache_analyzer.m passes per-user rates whose relevant slice is
53 * already the aggregate.
54 */
55
56#include <cstddef>
57#include <vector>
58
60#include "line/num/number.h"
61#include "line/util/error.h"
62#include "line/util/matrix.h"
63#include "line/util/rootfind.h"
64
65namespace line {
66namespace cache {
67
68namespace detail {
69
70/**
71 * Time-stationary probability of each item at each node, MATLAB's randprob,
72 * for the characteristic times x.
73 */
74template <class T>
75Matrix<T> lrua_randprob(const Matrix<T>& lambda, const std::vector<Matrix<T>>& R,
76 const std::vector<T>& x) {
77 using std::exp;
78 const std::size_t n = lambda.rows();
79 const std::size_t h = x.size();
80 const T zero = num_traits<T>::from_int(0);
81 const T one = num_traits<T>::from_int(1);
82 Matrix<T> randprob(n, h + 1, zero);
83
84 for (std::size_t i = 0; i < n; ++i) {
85 // no-arrivals item rationale: see _kb/09-ldes-and-cache.md (cpp port notes)
86 if (lambda(i, 0) == zero) {
87 randprob(i, 0) = one;
88 continue;
89 }
90 Matrix<T> trans(h + 1, h + 1, zero);
91 for (std::size_t j = 0; j <= h; ++j) {
92 for (std::size_t k = 0; k <= h; ++k) {
93 if (R[i](j, k) == zero) continue;
94 if (j == 0)
95 trans(j, k) = R[i](j, k);
96 else
97 trans(j, k) = (one - exp(T(-lambda(i, j) * x[j - 1]))) * R[i](j, k);
98 if (j != k) {
99 if (k == 0)
100 throw InputError(
101 "cache_ttl_lrua: the access graph routes into the not-cached node, "
102 "which has no characteristic time");
103 trans(k, j) = exp(T(-lambda(i, k) * x[k - 1]));
104 }
105 }
106 }
107 // Drop the nodes that no transition can reach (all-zero column).
108 std::vector<std::size_t> chain;
109 for (std::size_t c = 0; c <= h; ++c) {
110 bool allzero = true;
111 for (std::size_t r = 0; r <= h; ++r)
112 if (!(trans(r, c) == zero)) {
113 allzero = false;
114 break;
115 }
116 if (!allzero) chain.push_back(c);
117 }
118 if (chain.empty()) throw NumericError("cache_ttl_lrua: the item has no reachable node");
119 Matrix<T> P(chain.size(), chain.size());
120 for (std::size_t a = 0; a < chain.size(); ++a)
121 for (std::size_t b = 0; b < chain.size(); ++b) P(a, b) = trans(chain[a], chain[b]);
122 const std::vector<T> ssprob = mc::dtmc_solve(P);
123
124 std::vector<T> avgtime(chain.size());
125 T den = zero;
126 for (std::size_t a = 0; a < chain.size(); ++a) {
127 const std::size_t node = chain[a];
128 if (node == 0)
129 avgtime[a] = one / lambda(i, 0);
130 else
131 avgtime[a] = (one - exp(T(-lambda(i, node) * x[node - 1]))) / lambda(i, node);
132 den += ssprob[a] * avgtime[a];
133 }
134 if (den == zero) throw NumericError("cache_ttl_lrua: zero total holding time for an item");
135 for (std::size_t a = 0; a < chain.size(); ++a)
136 randprob(i, chain[a]) = ssprob[a] * avgtime[a] / den;
137 }
138 return randprob;
139}
140
141} // namespace detail
142
143/**
144 * @brief TTL (characteristic-time) approximation of an LRU cache whose lists
145 * form an arbitrary access graph.
146 *
147 * @param lambda (n x h+1) request rate of each item while it is at each node;
148 * column 0 is the rate while not cached
149 * @param R (n) access graphs, each (h+1 x h+1)
150 * @param m (h) list capacities
151 * @param tol relative tolerance on the characteristic times
152 * @param maxswp cap on Gauss-Seidel sweeps
153 * @return (n x h+1) time-stationary probabilities; column 0 is "not cached"
154 */
155template <class T>
156Matrix<T> cache_ttl_lrua(const Matrix<T>& lambda, const std::vector<Matrix<T>>& R,
157 const std::vector<T>& m, const T& tol, unsigned maxswp = 200) {
159 "cache_ttl_lrua requires transcendental arithmetic");
160 const std::size_t n = lambda.rows();
161 const std::size_t h = m.size();
162 if (n == 0) throw InputError("cache_ttl_lrua: no items");
163 if (h == 0) throw InputError("cache_ttl_lrua: no lists");
164 if (lambda.cols() != h + 1)
165 throw InputError("cache_ttl_lrua: the rate matrix must have h+1 columns");
166 if (R.size() != n) throw InputError("cache_ttl_lrua: one access graph per item is required");
167 for (std::size_t i = 0; i < n; ++i)
168 if (R[i].rows() != h + 1 || R[i].cols() != h + 1)
169 throw InputError("cache_ttl_lrua: each access graph must be (h+1 x h+1)");
170 const T zero = num_traits<T>::from_int(0);
171 for (std::size_t l = 0; l < h; ++l)
172 if (!(m[l] > zero)) throw InputError("cache_ttl_lrua: list capacities must be positive");
173
174 std::vector<T> x(h, num_traits<T>::from_int(1));
175 for (unsigned sweep = 0; sweep < maxswp; ++sweep) {
176 const std::vector<T> xold = x;
177 for (std::size_t l = 0; l < h; ++l) {
178 std::vector<T> work = x;
179 auto resid = [&](const T& v) {
180 work[l] = v;
181 const Matrix<T> P = detail::lrua_randprob(lambda, R, work);
182 T occ = zero;
183 for (std::size_t i = 0; i < n; ++i) occ += P(i, l + 1);
184 return T(occ - m[l]);
185 };
186 T lo = num_traits<T>::from_double(1e-12);
187 T hi = x[l] > lo ? x[l] : num_traits<T>::from_int(1);
188 if (!(resid(lo) < zero))
189 throw NumericError("cache_ttl_lrua: list occupancy exceeds its capacity even at a "
190 "vanishing characteristic time");
191 // A LIST THAT CANNOT FILL HAS NO FINITE CHARACTERISTIC TIME, and
192 // that is a state of the model rather than a numerical failure. The
193 // occupancy is increasing in x and bounded by the number of items
194 // the access graph can put in the list at a positive rate, so when
195 // that bound is below the capacity the residual never changes sign
196 // and the equation has no root: the list simply holds everything it
197 // ever sees. It is REACHED IN PRACTICE by SolverLN, whose first
198 // sweep solves the cache layer before any throughput has been
199 // propagated into it -- every rate is then zero, the occupancy is
200 // identically zero, and expanding the bracket only overflows.
201 //
202 // THE PROBE FOR THAT STATE MUST NOT UNDERFLOW, which is why it sits
203 // at 700/max(lambda) rather than at any larger number. Once
204 // exp(-lambda x) reaches exactly zero the item can no longer leave
205 // list l, the embedded chain loses that edge, and dtmc_solve is
206 // handed a chain whose recurrent class is a single absorbing node --
207 // which ctmc_solve TRIMS, returning occupancy zero for the very list
208 // that the limit fills completely. The probe would then read
209 // "cannot fill" for a list that saturates, and the next sweep, run
210 // with that value in hand, degenerates the chain outright. At
211 // 700/max(lambda) every exponential is still a normal double
212 // (exp(-700) ~ 1e-305), so the limit is evaluated to full precision
213 // with the chain intact. The reference reaches the same point by a
214 // different route: `cache_ttl_lrua.m` solves the same system with
215 // FSOLVE, a least-squares method that needs no sign change and
216 // returns its last iterate.
217 T colmax = zero;
218 for (std::size_t i = 0; i < n; ++i)
219 if (lambda(i, l + 1) > colmax) colmax = lambda(i, l + 1);
220 // No rate in the column: x[l] multiplies nothing, so nothing underflows.
221 const T saturated = colmax > zero
222 ? T(num_traits<T>::from_double(700.0) / colmax)
224 if (!(resid(saturated) > zero)) {
225 x[l] = saturated;
226 continue;
227 }
228 // Bracket by doubling, as bracket_expand does, but never past the
229 // probe: the sign change is known to lie at or below it.
230 const T two = num_traits<T>::from_int(2);
231 if (!(hi < saturated)) hi = saturated;
232 T fhi = resid(hi);
233 while (fhi < zero) {
234 hi *= two;
235 if (!(hi < saturated)) {
236 hi = saturated;
237 break;
238 }
239 fhi = resid(hi);
240 }
241 const RootResult<T> r = root_bisect<T>(resid, lo, hi, T(tol * hi), 400);
242 x[l] = r.root;
243 }
244 T rel = zero;
245 for (std::size_t l = 0; l < h; ++l) {
246 const T den = xold[l] > tol ? xold[l] : tol;
247 const T d = num_abs(T(x[l] - xold[l])) / den;
248 if (d > rel) rel = d;
249 }
250 if (rel < tol) break;
251 }
252 return detail::lrua_randprob(lambda, R, x);
253}
254
255} // namespace cache
256} // namespace line
257
258#endif // LINE_API_CACHE_TTL_LRUA_H
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
NumericError(const std::string &what)
Definition error.h:45
Equilibrium distribution of a discrete-time Markov chain, and stochastic complementation.
The exception types the port throws.
Dense matrix and non-owning view.
Matrix< T > cache_ttl_lrua(const Matrix< T > &lambda, const std::vector< Matrix< T > > &R, const std::vector< T > &m, const T &tol, unsigned maxswp=200)
TTL (characteristic-time) approximation of an LRU cache whose lists form an arbitrary access graph.
std::vector< T > dtmc_solve(const Matrix< T > &P)
Stationary distribution of a stochastic matrix P.
Definition dtmc_solve.h:106
T num_abs(const T &v)
Definition number.h:172
RootResult< T > root_bisect(F f, const T &a, const T &b, const T &tol, unsigned maxiter=200)
Bisection on a bracket with a sign change.
Definition rootfind.h:70
Number-type abstraction for the templated API port.
Deterministic scalar root finding.
Outcome of a scalar solve.
Definition rootfind.h:52
T root
best estimate of the root
Definition rootfind.h:53