LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
cache_miss_rmf.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_CACHE_MISS_RMF_H
6#define LINE_API_CACHE_CACHE_MISS_RMF_H
7
8/**
9 * @file
10 * @ingroup api_cache
11 * Refined mean field (RMF) miss rates of a multi-list RANDOM(m) cache.
12 *
13 * Templated port of matlab/src/api/cache/cache_miss_rmf.m, including its
14 * nested rmf_drift, rmf_jacobian, rmf_hessian, rmf_noise_matrix,
15 * rmf_fixed_point, rmf_dimension_reduction and rmf_expansion_steady_state.
16 *
17 * The model is a density dependent population process (DDPP) whose state
18 * x(i,k) is the probability that item i sits in list k, k = 0 meaning "not
19 * cached". A request for item i in list k promotes it to list k+1 and demotes
20 * a uniformly chosen occupant of list k+1, so the drift is
21 *
22 * flow(i,k) = p(i) x(i,k) - hit(k) x(i,k+1) / m(k+1),
23 * dx(i,k)/dt -= flow(i,k), dx(i,k+1)/dt += flow(i,k),
24 * hit(k) = sum_j p(j) x(j,k),
25 *
26 * for k = 0..h-1. The mean-field fixed point pi is the t -> infinity limit of
27 * that drift, and Gast's refinement adds the 1/N correction
28 *
29 * E[X] = pi + V/N + O(1/N^2), V = -(F')^-1 (1/2) sum_{b,c} F''_{bc} W_bc,
30 *
31 * with W the solution of the Lyapunov equation F' W + W F'^T + Q = 0 and Q the
32 * noise intensity of the DDPP. Reference: N. Gast, "Expected Values Estimated
33 * via Mean-Field Approximation are 1/N-Accurate", POMACS 2017.
34 *
35 * WHAT UNBLOCKED THIS. The fixed point is reached by integrating the drift to
36 * t = 1e4, which MATLAB does with ode15s. The drift is stiff: the per-item
37 * request rates p(i) of a Zipf-like popularity profile span several orders of
38 * magnitude, so the fast items equilibrate in O(1/p_max) while the slow ones
39 * need O(1/p_min), and an explicit integrator would be pinned to the fast
40 * scale for the whole horizon. The port now has line/util/ode.h, an adaptive
41 * Rosenbrock-4 with an embedded lower-order estimate, and this file uses it.
42 * The Jacobian is supplied analytically (rmf_jacobian is needed for the
43 * refinement anyway), so the integrator never forms a numeric one here.
44 *
45 * DIFFERENCES FROM THE REFERENCE. Three, all in the dimension reduction, and
46 * all forced by the same fact: the Jacobian of this drift is singular by
47 * construction (each item's occupancies are conserved, so the item-indicator
48 * vectors are exact left null vectors) and the refinement divides by its
49 * reduced version.
50 *
51 * 1. The fixed point is polished. The reference stops when ode15s's LOCAL
52 * error estimate meets AbsTol 1e-10, which leaves a residual drift of that
53 * order; this port runs a second integration pass from there at the
54 * tightest tolerances the arithmetic supports, which costs a handful of
55 * steps and leaves a residual of 1e-15. The miss rates move by about 1e-9,
56 * the rank decision below moves from meaningless to unambiguous.
57 * 2. The rank threshold is 1e-8 relative, not MATLAB's rank() threshold of
58 * max(size)*eps*sigma_1. A fixed point located to 1e-10 lifts one of the
59 * exact null directions to a singular value of about 1e-10, which MATLAB's
60 * threshold counts as nonzero; the reduced Jacobian then inherits it and
61 * the 1/N correction comes out orders of magnitude too large. The nonzero
62 * singular values here are O(0.1) and the null ones below 1e-13, so any
63 * threshold in that gap gives the same rank.
64 * 3. The null-space basis comes from LAPACK's SVD at T = double (the same
65 * quantity MATLAB reads out of svd(Fp)) and from a pivoted elimination
66 * plus Gram-Schmidt at any other T. Only the SUBSPACE affects the result:
67 * writing C = [C1; C2], the block C1 Fp D1 that the reduction keeps
68 * depends on C2 only through ker(C2), and the expansion V = D1 V_r
69 * likewise, so any basis of the same subspace gives the same V.
70 *
71 * MEASURED AGREEMENT (MATLAB R2025a, T = double), global miss rate M:
72 *
73 * lambda = [0.5 0.3 0.15 0.05; 0.1 0.2 0.3 0.4] (2 users, 4 items)
74 * m = [2] MATLAB 0.992377021789716 port 0.992377021789764 5e-14
75 * m = [1 2] MATLAB 0.490863272756674 port 0.490863283075726 2.1e-8
76 * lambda = [49 49 49 49 7 1 1]/205 (1 user, 7 items)
77 * m = [1 1 3] MATLAB 0.024017720168154 port 0.024017720912976 3.1e-8
78 * m = [3] MATLAB 0.348362022568998 port 0.345660745093153 7.8e-3
79 *
80 * The last row is not a discrepancy in the arithmetic but in which answer is
81 * returned: on that case the reference's refinement produces non-finite
82 * entries and cache_miss_rmf.m silently keeps the plain mean-field fixed point
83 * (its value agrees with the port's unrefined value to 1e-9), whereas the
84 * port's rank decision leaves the reduced Jacobian non-singular and the 1/N
85 * correction is applied, moving the miss rate by 0.8 percent. The result's
86 * `refined` flag says which of the two happened, so the caller can tell.
87 *
88 * When the reduced Jacobian is singular -- a small or non-hyperbolic fixed
89 * point -- MATLAB's linear solves emit a warning and return non-finite
90 * entries, which cache_miss_rmf.m detects and discards, keeping the plain
91 * mean-field fixed point. The port raises NumericError from the same solves
92 * and catches it in the same place, with the same outcome; the result carries
93 * a `refined` flag saying which of the two was used, which the reference does
94 * not expose.
95 *
96 * ARITHMETIC. Gated: the fixed point is reached by a tolerance-driven
97 * integration, so it is an approximation in any arithmetic.
98 *
99 * COST. The Hessian is a dense model_dim^3 tensor and the Lyapunov solve is a
100 * dense rk^2 by rk^2 system, exactly as in the reference. With n items and h
101 * lists model_dim = n(h+1), so the refinement is practical for tens of items
102 * and quickly stops being so; the plain mean-field path has no such limit.
103 */
104
105#include <cstddef>
106#include <limits>
107#include <type_traits>
108#include <vector>
109
110#include "line/num/number.h"
111#include "line/util/error.h"
112#include "line/util/eig.h"
113#include "line/util/linalg.h"
114#include "line/util/lu.h"
115#include "line/util/matrix.h"
116#include "line/util/lsoda.h"
117#include "line/util/ode.h"
119
120namespace line {
121namespace cache {
122
123/** Flat index of (item i, list k), k = 0 meaning "not cached" (rmf_index.m). */
124inline std::size_t cache_miss_rmf_index(std::size_t i, std::size_t k, std::size_t n_items) {
125 return i + k * n_items;
126}
127
128/** Return value of cache_miss_rmf, mirroring [M,MU,MI,pi0,tout,pi0_t,MU_t,xtraj]. */
129template <class T>
131 T M; ///< global miss rate
132 std::vector<T> MU; ///< (u) per-user miss rate
133 std::vector<T> MI; ///< (n_items) per-item miss rate
134 std::vector<T> pi0; ///< (n_items) per-item miss probability, clipped to [0,1]
135 std::vector<T> xss; ///< the occupancy the metrics were read from
136 bool refined = false; ///< true when the 1/N correction was accepted
137 std::vector<T> tout; ///< transient time grid, empty unless tspan was given
138 Matrix<T> pi0_t; ///< (n_items x nt) transient miss probability
139 Matrix<T> MU_t; ///< (u x nt) transient per-user miss rate
140 Matrix<T> xtraj; ///< (model_dim x nt) transient occupancy
141 /** Grid-independent average of the transient against a clock; empty unless one was given. */
143};
144
145namespace rmf_detail {
146
147/** hit rate of list `level`: sum_i p(i) x(i,level) (rmf_hit_rate.m). */
148template <class T>
149T hit_rate(const std::vector<T>& x, const std::vector<T>& p, std::size_t level,
150 std::size_t n_items) {
151 T hr = num_traits<T>::from_int(0);
152 for (std::size_t i = 0; i < n_items; ++i) hr += p[i] * x[cache_miss_rmf_index(i, level, n_items)];
153 return hr;
154}
155
156/** Mean-field drift F(x) (rmf_drift.m). */
157template <class T>
158std::vector<T> drift(const std::vector<T>& x, const std::vector<T>& p, const std::vector<T>& m,
159 std::size_t n_items, std::size_t h) {
160 const T zero = num_traits<T>::from_int(0);
161 const std::size_t model_dim = n_items * (h + 1);
162 std::vector<T> hr(h + 1, zero);
163 for (std::size_t k = 0; k <= h; ++k) hr[k] = hit_rate(x, p, k, n_items);
164 std::vector<T> dX(model_dim, zero);
165 for (std::size_t i = 0; i < n_items; ++i) {
166 for (std::size_t k = 0; k + 1 <= h; ++k) {
167 const std::size_t ik = cache_miss_rmf_index(i, k, n_items);
168 const std::size_t ik1 = cache_miss_rmf_index(i, k + 1, n_items);
169 const T flow = p[i] * x[ik] - hr[k] * x[ik1] / m[k];
170 dX[ik] -= flow;
171 dX[ik1] += flow;
172 }
173 }
174 return dX;
175}
176
177/**
178 * dF/dx at x (rmf_jacobian.m).
179 *
180 * REFERENCE DEFECT, reproduced deliberately. This is a transcription of
181 * rmf_jacobian in cache_miss_rmf.m, and that function is NOT the derivative of
182 * the rmf_drift it accompanies. Differentiating the drift
183 *
184 * flow(i,k) = p(i) x(i,k) - hit(k) x(i,k+1) / m(k+1), hit(k) = sum_j p(j) x(j,k)
185 *
186 * gives d flow / d x(j,k+1) = -hit(k)/m(k+1) for j = i and ZERO for j != i,
187 * whereas rmf_jacobian adds a further -p(i) x(i,k)/m(k+1) for EVERY j.
188 * Those extra entries belong to the pairwise form of the drift, in which the
189 * demoted item is chosen explicitly, not to the aggregated form that rmf_drift
190 * implements. The discrepancy is real and reproducible: a central difference of
191 * rmf_drift disagrees with rmf_jacobian at those entries by about 10 percent
192 * (see the test, which pins the reference's values rather than the derivative).
193 *
194 * The port keeps the reference's formula because the refined mean-field
195 * correction is DEFINED by it in the reference and changing it would change the
196 * ported answer. It is not used as an integration Jacobian anywhere here: the
197 * fixed point is integrated with a numeric Jacobian, which is what the
198 * reference's own ode15s call does.
199 */
200template <class T>
201Matrix<T> jacobian(const std::vector<T>& x, const std::vector<T>& p, const std::vector<T>& m,
202 std::size_t n_items, std::size_t h) {
203 const T zero = num_traits<T>::from_int(0);
204 const std::size_t model_dim = n_items * (h + 1);
205 std::vector<T> hr(h + 1, zero);
206 for (std::size_t k = 0; k <= h; ++k) hr[k] = hit_rate(x, p, k, n_items);
207 Matrix<T> Fp(model_dim, model_dim, zero);
208 for (std::size_t i = 0; i < n_items; ++i) {
209 for (std::size_t k = 0; k + 1 <= h; ++k) {
210 const std::size_t ik = cache_miss_rmf_index(i, k, n_items);
211 const std::size_t ik1 = cache_miss_rmf_index(i, k + 1, n_items);
212 Fp(ik, ik) -= p[i];
213 Fp(ik1, ik) += p[i];
214 Fp(ik, ik1) += hr[k] / m[k];
215 Fp(ik1, ik1) -= hr[k] / m[k];
216 for (std::size_t j = 0; j < n_items; ++j) {
217 const std::size_t jk = cache_miss_rmf_index(j, k, n_items);
218 const std::size_t jk1 = cache_miss_rmf_index(j, k + 1, n_items);
219 Fp(ik, jk1) -= p[i] * x[ik] / m[k];
220 Fp(ik1, jk1) += p[i] * x[ik] / m[k];
221 Fp(ik, jk) += p[j] * x[ik1] / m[k];
222 Fp(ik1, jk) -= p[j] * x[ik1] / m[k];
223 }
224 }
225 }
226 return Fp;
227}
228
229/**
230 * d^2F/dx^2 (rmf_hessian.m), stored flat: H[(a*model_dim + b)*model_dim + c] is
231 * d^2 F_a / (dx_b dx_c). The drift is quadratic, so the Hessian does not
232 * depend on x, exactly as in the reference.
233 */
234template <class T>
235std::vector<T> hessian(const std::vector<T>& p, const std::vector<T>& m, std::size_t n_items,
236 std::size_t h) {
237 const T zero = num_traits<T>::from_int(0);
238 const std::size_t md = n_items * (h + 1);
239 std::vector<T> H(md * md * md, zero);
240 const auto at = [md](std::size_t a, std::size_t b, std::size_t c) {
241 return (a * md + b) * md + c;
242 };
243 for (std::size_t i = 0; i < n_items; ++i) {
244 for (std::size_t k = 0; k + 1 <= h; ++k) {
245 const std::size_t ik = cache_miss_rmf_index(i, k, n_items);
246 const std::size_t ik1 = cache_miss_rmf_index(i, k + 1, n_items);
247 for (std::size_t j = 0; j < n_items; ++j) {
248 if (j == i) continue;
249 const std::size_t jk = cache_miss_rmf_index(j, k, n_items);
250 const std::size_t jk1 = cache_miss_rmf_index(j, k + 1, n_items);
251 H[at(ik, jk, ik1)] += p[j] / m[k];
252 H[at(ik, ik1, jk)] += p[j] / m[k];
253 H[at(ik, jk1, ik)] -= p[i] / m[k];
254 H[at(ik, ik, jk1)] -= p[i] / m[k];
255 H[at(ik1, jk, ik1)] -= p[j] / m[k];
256 H[at(ik1, ik1, jk)] -= p[j] / m[k];
257 H[at(ik1, jk1, ik)] += p[i] / m[k];
258 H[at(ik1, ik, jk1)] += p[i] / m[k];
259 }
260 }
261 }
262 return H;
263}
264
265/** Noise intensity Q(x) of the DDPP (rmf_noise_matrix.m). */
266template <class T>
267Matrix<T> noise_matrix(const std::vector<T>& x, const std::vector<T>& p, const std::vector<T>& m,
268 std::size_t n_items, std::size_t h) {
269 const T zero = num_traits<T>::from_int(0);
270 const std::size_t md = n_items * (h + 1);
271 Matrix<T> Q(md, md, zero);
272 const int signs[4] = {-1, 1, 1, -1};
273 for (std::size_t i = 0; i < n_items; ++i) {
274 for (std::size_t k = 0; k + 1 <= h; ++k) {
275 for (std::size_t j = 0; j < n_items; ++j) {
276 const T rate = p[i] * x[cache_miss_rmf_index(i, k, n_items)] *
277 x[cache_miss_rmf_index(j, k + 1, n_items)] / m[k];
278 const std::size_t idx[4] = {cache_miss_rmf_index(i, k, n_items),
279 cache_miss_rmf_index(j, k, n_items),
280 cache_miss_rmf_index(i, k + 1, n_items),
281 cache_miss_rmf_index(j, k + 1, n_items)};
282 for (int ia = 0; ia < 4; ++ia)
283 for (int ib = 0; ib < 4; ++ib)
284 Q(idx[ia], idx[ib]) +=
285 rate * num_traits<T>::from_int(signs[ia] * signs[ib]);
286 }
287 }
288 }
289 return Q;
290}
291
292/**
293 * Mean-field fixed point by integrating the drift to tmax (rmf_fixed_point.m).
294 * The reference uses ode15s with RelTol 1e-8 and AbsTol 1e-10 over [0,10000];
295 * the port uses the same horizon and the same tolerances with ode_rosenbrock4
296 * and the analytic Jacobian.
297 */
298template <class T>
299std::vector<T> fixed_point(const std::vector<T>& x0, const std::vector<T>& p,
300 const std::vector<T>& m, std::size_t n_items, std::size_t h,
301 const T& tmax, const T& rtol, const T& atol) {
302 OdeOptions<T> opt;
303 opt.rtol = rtol;
304 opt.atol = atol;
305 opt.store_trajectory = false;
306 const auto f = [&](const T& t, const std::vector<T>& x) {
307 (void)t;
308 return drift(x, p, m, n_items, h);
309 };
310 // numeric-Jacobian rationale: see _kb/09-ldes-and-cache.md (cpp port notes)
311 return ode_rosenbrock4(f, T(num_traits<T>::from_int(0)), tmax, x0, opt).final_state();
312}
313
314/** fixed_point with the reference tolerances, RelTol 1e-8 and AbsTol 1e-10. */
315template <class T>
316std::vector<T> fixed_point(const std::vector<T>& x0, const std::vector<T>& p,
317 const std::vector<T>& m, std::size_t n_items, std::size_t h,
318 const T& tmax) {
319 return fixed_point(x0, p, m, n_items, h, tmax, T(num_traits<T>::from_double(1e-8)),
320 T(num_traits<T>::from_double(1e-10)));
321}
322
323/**
324 * Solve the Lyapunov equation F W + W F^T + Q = 0 by vectorization.
325 *
326 * The vectorized operator is (I (x) F + F (x) I) acting on vec(W); with W
327 * stored row-major, row (a,b) of the system reads
328 * sum_c F(a,c) W(c,b) + sum_c F(b,c) W(a,c) = -Q(a,b).
329 * The system is r^2 by r^2 and is solved with the port's LU. MATLAB calls
330 * lyap(), which uses a Bartels-Stewart Schur factorization; the two compute
331 * the same W, and the Schur route is the faster one, not a different answer.
332 * The vectorized route is used here because it needs nothing beyond the LU
333 * that the port already has, and because it fails loudly (a singular matrix
334 * throws) when F has a zero or a symmetric pair of eigenvalues, which is the
335 * case the caller must detect and reject.
336 */
337template <class T>
338Matrix<T> lyapunov(const Matrix<T>& F, const Matrix<T>& Q) {
339 const std::size_t r = F.rows();
340 const T zero = num_traits<T>::from_int(0);
341 Matrix<T> A(r * r, r * r, zero);
342 std::vector<T> rhs(r * r, zero);
343 for (std::size_t a = 0; a < r; ++a)
344 for (std::size_t b = 0; b < r; ++b) {
345 const std::size_t row = a * r + b;
346 for (std::size_t c = 0; c < r; ++c) {
347 A(row, c * r + b) += F(a, c);
348 A(row, a * r + c) += F(b, c);
349 }
350 rhs[row] = -Q(a, b);
351 }
352 const std::vector<T> w = line::solve(A, rhs);
353 Matrix<T> W(r, r, zero);
354 for (std::size_t a = 0; a < r; ++a)
355 for (std::size_t b = 0; b < r; ++b) W(a, b) = w[a * r + b];
356 return W;
357}
358
359/**
360 * Numeric rank and an orthonormal basis of the LEFT null space of Fp, by
361 * Gaussian elimination with full pivoting on Fp^T followed by Gram-Schmidt.
362 *
363 * MATLAB gets both from svd(Fp): rk = rank(Fp) and the trailing left singular
364 * vectors. The port cannot, because the port's SVD entry point is LAPACK-backed
365 * and double-only while this header is templated; and it does not need to,
366 * because only the SUBSPACE matters, not the basis of it (see the note at the
367 * top of this file). What does matter is the rank DECISION, which is a
368 * tolerance comparison in both codes: MATLAB thresholds the singular values at
369 * max(size)*eps*sigma_max, this thresholds the elimination pivots at
370 * n*eps*max|Fp|. The two agree except on a matrix whose rank is genuinely
371 * ambiguous at that scale, where neither answer is more correct than the other.
372 *
373 * The left null space is where the mean-field conservation laws live: each
374 * item's occupancies sum to one, so the n item-indicator vectors are always in
375 * it, and a symmetric popularity profile (items with equal request rates) adds
376 * more. That is why the rank cannot simply be taken as model_dim - n_items.
377 */
378template <class T>
379struct NullSpace {
380 std::size_t rank = 0;
381 std::vector<std::vector<T>> basis; ///< orthonormal rows spanning {w : w^T Fp = 0}
382};
383
384template <class T>
385NullSpace<T> left_null_space(const Matrix<T>& Fp) {
386 const std::size_t n = Fp.rows();
387 const T zero = num_traits<T>::from_int(0);
388 // Work on M = Fp^T so that its null vectors are the left null vectors of Fp.
389 Matrix<T> M(n, n, zero);
390 T scale = zero;
391 for (std::size_t i = 0; i < n; ++i)
392 for (std::size_t j = 0; j < n; ++j) {
393 M(i, j) = Fp(j, i);
394 const T a = num_abs(Fp(j, i));
395 if (a > scale) scale = a;
396 }
397 if (scale == zero) scale = num_traits<T>::from_int(1);
398 // Rank threshold. NOT the machine-epsilon one: see left_null_space_svd for
399 // why a relative 1e-8 gap is the right question to ask here.
400 const T tol = scale * num_traits<T>::from_double(1e-8);
401
402 std::vector<std::size_t> col_of_pivot;
403 std::vector<std::size_t> perm(n);
404 for (std::size_t i = 0; i < n; ++i) perm[i] = i;
405 std::size_t row = 0;
406 for (std::size_t col = 0; col < n && row < n; ++col) {
407 std::size_t p = row;
408 T best = num_abs(M(row, col));
409 for (std::size_t i = row + 1; i < n; ++i) {
410 const T a = num_abs(M(i, col));
411 if (a > best) {
412 best = a;
413 p = i;
414 }
415 }
416 if (best <= tol) continue; // no pivot in this column: it is free
417 if (p != row)
418 for (std::size_t j = 0; j < n; ++j) std::swap(M(row, j), M(p, j));
419 const T d = M(row, col);
420 for (std::size_t j = 0; j < n; ++j) M(row, j) = M(row, j) / d;
421 for (std::size_t i = 0; i < n; ++i) {
422 if (i == row) continue;
423 const T f = M(i, col);
424 if (f == zero) continue;
425 for (std::size_t j = 0; j < n; ++j) M(i, j) -= f * M(row, j);
426 }
427 col_of_pivot.push_back(col);
428 ++row;
429 }
430
431 NullSpace<T> ns;
432 ns.rank = col_of_pivot.size();
433 std::vector<bool> is_pivot(n, false);
434 for (std::size_t c : col_of_pivot) is_pivot[c] = true;
435 // One basis vector per free column, from the reduced row echelon form.
436 for (std::size_t free_col = 0; free_col < n; ++free_col) {
437 if (is_pivot[free_col]) continue;
438 std::vector<T> v(n, zero);
439 v[free_col] = num_traits<T>::from_int(1);
440 for (std::size_t r = 0; r < col_of_pivot.size(); ++r)
441 v[col_of_pivot[r]] = -M(r, free_col);
442 ns.basis.push_back(v);
443 }
444 // Gram-Schmidt, so that the basis is orthonormal like MATLAB's.
445 using std::sqrt;
446 for (std::size_t i = 0; i < ns.basis.size(); ++i) {
447 for (std::size_t k = 0; k < i; ++k) {
448 T dot = zero;
449 for (std::size_t j = 0; j < n; ++j) dot += ns.basis[i][j] * ns.basis[k][j];
450 for (std::size_t j = 0; j < n; ++j) ns.basis[i][j] -= dot * ns.basis[k][j];
451 }
452 T nrm2 = zero;
453 for (std::size_t j = 0; j < n; ++j) nrm2 += ns.basis[i][j] * ns.basis[i][j];
454 const T nrm = sqrt(nrm2);
455 if (nrm <= tol) throw NumericError("cache_miss_rmf: the null-space basis degenerated");
456 for (std::size_t j = 0; j < n; ++j) ns.basis[i][j] = ns.basis[i][j] / nrm;
457 }
458 return ns;
459}
460
461/**
462 * The same thing from the SVD, which is what the reference uses.
463 *
464 * This matters more than a change of basis normally would. The Jacobian at the
465 * mean-field fixed point is very ill conditioned: on the four-item two-list
466 * case below its singular values are 7.5e-1 ... 1.7e-1, then 5.4e-10, then
467 * five at 1e-17. The 5.4e-10 one is a null direction that the finite accuracy
468 * of the fixed point has lifted off zero, so it counts as a nonzero singular
469 * value under both MATLAB's rank tolerance and any other, and the reduced
470 * Jacobian inherits it. Everything downstream then divides by it, and the 1/N
471 * correction depends on which basis of the (numerically ambiguous) null space
472 * was chosen. Reproducing the reference's numbers therefore requires
473 * reproducing its basis, not merely its subspace. This routine is used when
474 * LAPACK is available and T is double; the templated elimination above is the
475 * fallback, and the two agree whenever the fixed point is hyperbolic and the
476 * rank decision is unambiguous.
477 */
478inline NullSpace<double> left_null_space_svd(const Matrix<double>& Fp) {
479#ifndef LINE_MP_HAVE_LAPACK
480 return left_null_space(Fp);
481#else
482 const std::size_t n = Fp.rows();
483 std::vector<double> a(n * n);
484 for (std::size_t i = 0; i < n; ++i)
485 for (std::size_t j = 0; j < n; ++j) a[j * n + i] = Fp(i, j);
486 const int ni = static_cast<int>(n);
487 std::vector<double> s(n), u(n * n), vt(1);
488 int info = 0, lwork = -1;
489 double wopt = 0.0;
490 const int one = 1;
491 dgesvd_("A", "N", &ni, &ni, a.data(), &ni, s.data(), u.data(), &ni, vt.data(), &one, &wopt,
492 &lwork, &info);
493 if (info != 0) throw NumericError("cache_miss_rmf: LAPACK workspace query failed");
494 lwork = static_cast<int>(wopt);
495 std::vector<double> work(static_cast<std::size_t>(lwork));
496 dgesvd_("A", "N", &ni, &ni, a.data(), &ni, s.data(), u.data(), &ni, vt.data(), &one,
497 work.data(), &lwork, &info);
498 if (info != 0) throw NumericError("cache_miss_rmf: LAPACK dgesvd failed to converge");
499 // rank threshold rationale: see _kb/09-ldes-and-cache.md (cpp port notes)
500 const double tol = 1e-8 * s[0];
501 NullSpace<double> ns;
502 ns.rank = 0;
503 for (double v : s)
504 if (v > tol) ++ns.rank;
505 for (std::size_t c = ns.rank; c < n; ++c) {
506 std::vector<double> row(n, 0.0);
507 for (std::size_t i = 0; i < n; ++i) row[i] = u[c * n + i];
508 ns.basis.push_back(row);
509 }
510 return ns;
511#endif
512}
513
514/** Dispatch: the SVD basis at double, the elimination basis otherwise. */
515template <class T>
516NullSpace<T> left_null_space_for(const Matrix<T>& Fp) {
517 return left_null_space(Fp);
518}
519
520template <>
521inline NullSpace<double> left_null_space_for<double>(const Matrix<double>& Fp) {
522 return left_null_space_svd(Fp);
523}
524
525// ---------------------------------------------------------------------------
526// The general access graph (`accost`).
527// ---------------------------------------------------------------------------
528/**
529 * `rmf_linear_graph`: the standard chain. Row 0 is miss admission (column 0 is
530 * reject, column 1+l admit to list l), row 1+i is a hit in list i, and the top
531 * list self-loops because a hit there moves nothing.
532 */
533template <class T>
534Matrix<T> linear_graph(std::size_t h) {
535 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
536 Matrix<T> g(h + 1, h + 1, zero);
537 g(0, 1) = one;
538 for (std::size_t a = 1; a + 1 <= h; ++a) g(a, a + 1) = one;
539 g(h, h) = one;
540 return g;
541}
542
543/**
544 * `rmf_build_item_graphs`: one (h+1)x(h+1) graph per item, the per-user graphs
545 * averaged by request rate and row-normalized.
546 *
547 * RETURNS EMPTY WHEN THE RESULT IS THE LINEAR CHAIN, and that is not an
548 * optimization. The linear chain is the only case for which the 1/N REFINEMENT
549 * is defined here -- its Jacobian, Hessian and noise matrix are all written for
550 * the chain drift -- so the caller must be able to tell "no graph was declared,
551 * take the refined path" from "a graph was declared, take the plain mean-field
552 * fixed point of the general drift". An empty return is that signal.
553 */
554template <class T>
555std::vector<Matrix<T> > build_item_graphs(const std::vector<std::vector<Matrix<T> > >& accost,
556 const Matrix<T>& lambda, std::size_t n, std::size_t h) {
557 std::vector<Matrix<T> > G;
558 if (accost.empty()) return G;
559 const T zero = num_traits<T>::from_int(0);
560 const Matrix<T> lin = linear_graph<T>(h);
561 const std::size_t u = accost.size();
562 std::vector<Matrix<T> > Gc(n, lin);
563 bool is_linear = true;
564 for (std::size_t k = 0; k < n; ++k) {
565 Matrix<T> num(h + 1, h + 1, zero);
566 T den = zero;
567 for (std::size_t v = 0; v < u; ++v) {
568 if (k >= accost[v].size()) continue;
569 const Matrix<T>& gvk = accost[v][k];
570 if (gvk.rows() == 0) continue;
571 if (gvk.rows() != h + 1 || gvk.cols() != h + 1)
572 throw InputError("cache_miss_rmf: an access graph is not (h+1)x(h+1)");
573 double w = (v < lambda.rows() && k < lambda.cols())
574 ? num_traits<T>::to_double(lambda(v, k))
575 : 0.0;
576 if (!std::isfinite(w)) w = 0.0;
577 const T wv = num_traits<T>::from_double(w);
578 for (std::size_t a = 0; a <= h; ++a)
579 for (std::size_t b = 0; b <= h; ++b) num(a, b) += wv * gvk(a, b);
580 den += wv;
581 }
582 Matrix<T> gk = lin;
583 if (den > zero) {
584 for (std::size_t a = 0; a <= h; ++a)
585 for (std::size_t b = 0; b <= h; ++b) gk(a, b) = num(a, b) / den;
586 } else if (!accost.empty() && k < accost[0].size() && accost[0][k].rows() == h + 1) {
587 gk = accost[0][k];
588 }
589 for (std::size_t a = 0; a <= h; ++a) {
590 T srow = zero;
591 for (std::size_t b = 0; b <= h; ++b) srow += gk(a, b);
592 if (srow > zero)
593 for (std::size_t b = 0; b <= h; ++b) gk(a, b) = gk(a, b) / srow;
594 }
595 Gc[k] = gk;
596 for (std::size_t a = 0; a <= h && is_linear; ++a)
597 for (std::size_t b = 0; b <= h && is_linear; ++b)
598 if (std::fabs(num_traits<T>::to_double(gk(a, b)) -
599 num_traits<T>::to_double(lin(a, b))) >= 1e-9)
600 is_linear = false;
601 }
602 if (!is_linear) G = Gc;
603 return G;
604}
605
606/**
607 * `rmf_drift_graph`: the general RANDOM(m) drift under a per-item access graph.
608 *
609 * A miss is admitted to list i with probability G[k](0,1+i) and a hit in list s
610 * promotes to list i with probability G[k](1+s,1+i); the occupant it displaces
611 * is drawn UNIFORMLY from the target list, which is the RR sample path
612 * (`State.afterEventCache`) and is why every displacement term carries the
613 * 1/m(i) factor. It reduces to the chain drift above when G is the linear
614 * graph, which is what makes `build_item_graphs`'s emptiness test sound.
615 */
616template <class T>
617std::vector<T> drift_graph(const std::vector<T>& x_in, const std::vector<T>& p,
618 const std::vector<Matrix<T> >& G, const std::vector<T>& m,
619 std::size_t n, std::size_t h) {
620 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
621 const std::size_t model_dim = n * (h + 1);
622 std::vector<T> x = x_in;
623 for (std::size_t a = 0; a < x.size(); ++a) {
624 if (x[a] < zero) x[a] = zero;
625 if (x[a] > one) x[a] = one;
626 }
627 // A(s,i): total rate of insertion or promotion into list i out of list s.
628 Matrix<T> A(h + 1, h + 1, zero);
629 for (std::size_t s = 0; s <= h; ++s)
630 for (std::size_t j = 0; j < n; ++j) {
631 const T xjs = x[cache_miss_rmf_index(j, s, n)];
632 if (xjs == zero) continue;
633 for (std::size_t i = 1; i <= h; ++i) A(s, i) += p[j] * xjs * G[j](s, i);
634 }
635 std::vector<T> dX(model_dim, zero);
636 for (std::size_t k = 0; k < n; ++k) {
637 const T outk = x[cache_miss_rmf_index(k, 0, n)];
638 for (std::size_t i = 1; i <= h; ++i) {
639 const T xki = x[cache_miss_rmf_index(k, i, n)];
640 T infl = p[k] * outk * G[k](0, i);
641 for (std::size_t s = 1; s + 1 <= i; ++s)
642 infl += p[k] * x[cache_miss_rmf_index(k, s, n)] * G[k](s, i);
643 for (std::size_t b = i + 1; b <= h; ++b)
644 infl += A(i, b) * x[cache_miss_rmf_index(k, b, n)] / m[b - 1];
645 T outfl = p[k] * xki * T(one - G[k](i, i));
646 T disp = zero;
647 for (std::size_t s = 0; s + 1 <= i; ++s) disp += A(s, i);
648 outfl += disp * xki / m[i - 1];
649 dX[cache_miss_rmf_index(k, i, n)] += infl - outfl;
650 }
651 T acc = zero;
652 for (std::size_t i = 1; i <= h; ++i) acc += dX[cache_miss_rmf_index(k, i, n)];
653 dX[cache_miss_rmf_index(k, 0, n)] = -acc;
654 }
655 return dX;
656}
657
658/**
659 * `rmf_fixed_point_graph`: the plain mean-field fixed point of the general
660 * drift, at the reference's own horizon of 20000 (twice the chain's, because
661 * the general drift has no refinement to fall back on if it stops short).
662 */
663template <class T>
664std::vector<T> fixed_point_graph(const std::vector<T>& x0, const std::vector<T>& p,
665 const std::vector<Matrix<T> >& G, const std::vector<T>& m,
666 std::size_t n, std::size_t h) {
667 OdeOptions<T> opt;
668 opt.rtol = num_traits<T>::from_double(1e-8);
669 opt.atol = num_traits<T>::from_double(1e-10);
670 opt.store_trajectory = false;
671 const auto f = [&](const T& t, const std::vector<T>& x) {
672 (void)t;
673 return drift_graph(x, p, G, m, n, h);
674 };
675 return ode_rosenbrock4(f, T(num_traits<T>::from_int(0)),
676 T(num_traits<T>::from_int(20000)), x0, opt)
677 .final_state();
678}
679
680} // namespace rmf_detail
681
682/**
683 * Refined mean-field miss rates of a RANDOM(m) multi-list cache.
684 *
685 * @param gamma item access factors. Present for signature compatibility with
686 * cache_miss_rmf.m, which marks it unused and reads
687 * only its size; nothing here depends on it either.
688 * @param m_in (h) list capacities
689 * @param lambda (u x n_items) per-user per-item request rates. The MATLAB
690 * argument is a three-dimensional array and the function reads
691 * only its first page, lambda(v,:,1); this is that page.
692 * @param tmax integration horizon for the fixed point (reference: 1e4)
693 * @param accost per-(user,item) access graph, each an (h+1)x(h+1) matrix; empty
694 * is the linear chain. A NON-LINEAR graph switches the solve to
695 * the general drift AND drops the 1/N refinement, exactly as the
696 * reference does: the refinement's Jacobian, Hessian and noise
697 * matrix are written for the chain drift, so applying it to
698 * another drift would correct the wrong system.
699 */
700template <class T>
701CacheMissRmfResult<T> cache_miss_rmf(const std::vector<T>& gamma, const std::vector<int>& m_in,
702 const Matrix<T>& lambda, const T& tmax,
703 const std::vector<std::vector<Matrix<T> > >& accost) {
705 "cache_miss_rmf requires transcendental arithmetic: its fixed point is reached "
706 "by a tolerance-driven integration of the mean-field drift");
707 (void)gamma;
708 const T zero = num_traits<T>::from_int(0);
709 const T one = num_traits<T>::from_int(1);
710 const std::size_t u = lambda.rows();
711 const std::size_t n_items = lambda.cols();
712 const std::size_t h = m_in.size();
713 if (u == 0 || n_items == 0) throw InputError("cache_miss_rmf: empty request-rate matrix");
714 if (h == 0) throw InputError("cache_miss_rmf: at least one cache list is required");
715
716 std::vector<T> m(h, zero);
717 for (std::size_t k = 0; k < h; ++k) {
718 if (m_in[k] <= 0) throw InputError("cache_miss_rmf: a list has non-positive capacity");
719 m[k] = num_traits<T>::from_int(static_cast<long>(m_in[k]));
720 }
721
722 // non-finite rate rejection rationale: see _kb/09-ldes-and-cache.md (cpp port notes)
723 std::vector<T> lam_i(n_items, zero);
724 T lam_tot = zero;
725 for (std::size_t v = 0; v < u; ++v)
726 for (std::size_t i = 0; i < n_items; ++i) {
727 lam_i[i] += lambda(v, i);
728 lam_tot += lambda(v, i);
729 }
730 if (lam_tot == zero) throw InputError("cache_miss_rmf: all request rates are zero");
731 std::vector<T> p(n_items, zero);
732 for (std::size_t i = 0; i < n_items; ++i) p[i] = lam_i[i] / lam_tot;
733
734 const std::size_t model_dim = n_items * (h + 1);
735
736 // Initial occupancy: the first m(1) items in list 1, the next m(2) in list
737 // 2, and so on; every remaining item outside the cache.
738 std::vector<T> x0(model_dim, zero);
739 std::size_t obj = 0;
740 for (std::size_t k = 1; k <= h; ++k)
741 for (int jj = 0; jj < m_in[k - 1]; ++jj) {
742 ++obj;
743 if (obj <= n_items) x0[cache_miss_rmf_index(obj - 1, k, n_items)] = one;
744 }
745 for (std::size_t i = obj; i < n_items; ++i) x0[cache_miss_rmf_index(i, 0, n_items)] = one;
746
748 const std::vector<Matrix<T> > G = rmf_detail::build_item_graphs(accost, lambda, n_items, h);
749 if (!G.empty()) {
750 // A declared access graph takes the general drift and the plain fixed
751 // point; `refined` stays false, which is the honest report.
752 res.xss = rmf_detail::fixed_point_graph(x0, p, G, m, n_items, h);
753 res.pi0.assign(n_items, zero);
754 for (std::size_t i = 0; i < n_items; ++i) {
755 T v = res.xss[cache_miss_rmf_index(i, 0, n_items)];
756 if (v < zero) v = zero;
757 if (v > one) v = one;
758 res.pi0[i] = v;
759 }
760 res.MI.assign(n_items, zero);
761 res.M = zero;
762 for (std::size_t i = 0; i < n_items; ++i) {
763 res.MI[i] = lam_i[i] * res.pi0[i];
764 res.M += res.MI[i];
765 }
766 res.MU.assign(u, zero);
767 for (std::size_t v = 0; v < u; ++v) {
768 T s = zero;
769 for (std::size_t i = 0; i < n_items; ++i) s += lambda(v, i) * res.pi0[i];
770 res.MU[v] = s;
771 }
772 return res;
773 }
774 // two-pass ODE integration rationale: see _kb/09-ldes-and-cache.md (cpp port notes)
775 std::vector<T> xss = rmf_detail::fixed_point(x0, p, m, n_items, h, tmax);
776 xss = rmf_detail::fixed_point(xss, p, m, n_items, h, tmax,
779
780 // 1/N refinement failure rationale: see _kb/09-ldes-and-cache.md (cpp port notes)
781 try {
782 const Matrix<T> Fp = rmf_detail::jacobian(xss, p, m, n_items, h);
783 const rmf_detail::NullSpace<T> ns = rmf_detail::left_null_space_for(Fp);
784 const std::size_t rk = ns.rank;
785 if (rk == 0 || rk >= model_dim)
786 throw NumericError("cache_miss_rmf: the reduction is degenerate");
787 const std::vector<T> Fpp = rmf_detail::hessian(p, m, n_items, h);
788 const Matrix<T> Q = rmf_detail::noise_matrix(xss, p, m, n_items, h);
789
790 // change-of-basis rationale: see _kb/09-ldes-and-cache.md (cpp port notes)
791 Matrix<T> C(model_dim, model_dim, zero);
792 {
793 std::size_t d = 0;
794 for (std::size_t l = 0; l <= h && d < rk; ++l)
795 for (std::size_t i = 0; i + 1 < n_items && d < rk; ++i, ++d)
796 C(d, cache_miss_rmf_index(i, l, n_items)) = one;
797 if (d != rk) throw NumericError("cache_miss_rmf: the reduction basis is too small");
798 }
799 if (ns.basis.size() != model_dim - rk)
800 throw NumericError("cache_miss_rmf: the null-space basis has the wrong size");
801 for (std::size_t i = 0; i < ns.basis.size(); ++i)
802 for (std::size_t j = 0; j < model_dim; ++j) C(rk + i, j) = ns.basis[i][j];
803 const Matrix<T> Cinv = inverse(C);
804
805 Matrix<T> Fp_r(rk, rk, zero);
806 {
807 const Matrix<T> tmp = matmul(C, matmul(Fp, Cinv));
808 for (std::size_t a = 0; a < rk; ++a)
809 for (std::size_t b = 0; b < rk; ++b) Fp_r(a, b) = tmp(a, b);
810 }
811 Matrix<T> Q_r(rk, rk, zero);
812 {
813 Matrix<T> Ct(model_dim, model_dim, zero);
814 for (std::size_t a = 0; a < model_dim; ++a)
815 for (std::size_t b = 0; b < model_dim; ++b) Ct(a, b) = C(b, a);
816 const Matrix<T> tmp = matmul(C, matmul(Q, Ct));
817 for (std::size_t a = 0; a < rk; ++a)
818 for (std::size_t b = 0; b < rk; ++b) Q_r(a, b) = tmp(a, b);
819 }
820
821 // Reduced Hessian, by the same three contractions the reference does.
822 const auto Hat = [model_dim](std::size_t a, std::size_t b, std::size_t c) {
823 return (a * model_dim + b) * model_dim + c;
824 };
825 std::vector<T> tmp1(rk * model_dim * model_dim, zero);
826 for (std::size_t a = 0; a < rk; ++a)
827 for (std::size_t j = 0; j < model_dim; ++j)
828 for (std::size_t k = 0; k < model_dim; ++k) {
829 T s = zero;
830 for (std::size_t i = 0; i < model_dim; ++i) s += C(a, i) * Fpp[Hat(i, j, k)];
831 tmp1[(a * model_dim + j) * model_dim + k] = s;
832 }
833 std::vector<T> tmp2(rk * rk * model_dim, zero);
834 for (std::size_t a = 0; a < rk; ++a)
835 for (std::size_t b = 0; b < rk; ++b)
836 for (std::size_t k = 0; k < model_dim; ++k) {
837 T s = zero;
838 for (std::size_t j = 0; j < model_dim; ++j)
839 s += tmp1[(a * model_dim + j) * model_dim + k] * Cinv(j, b);
840 tmp2[(a * rk + b) * model_dim + k] = s;
841 }
842 std::vector<T> Fpp_r(rk * rk * rk, zero);
843 for (std::size_t a = 0; a < rk; ++a)
844 for (std::size_t b = 0; b < rk; ++b)
845 for (std::size_t c = 0; c < rk; ++c) {
846 T s = zero;
847 for (std::size_t k = 0; k < model_dim; ++k)
848 s += tmp2[(a * rk + b) * model_dim + k] * Cinv(k, c);
849 Fpp_r[(a * rk + b) * rk + c] = s;
850 }
851
852 const Matrix<T> W_r = rmf_detail::lyapunov(Fp_r, Q_r);
853
854 std::vector<T> C_r(rk, zero);
855 for (std::size_t a = 0; a < rk; ++a) {
856 T s = zero;
857 for (std::size_t b = 0; b < rk; ++b)
858 for (std::size_t c = 0; c < rk; ++c) s += Fpp_r[(a * rk + b) * rk + c] * W_r(b, c);
859 C_r[a] = s;
860 }
861 std::vector<T> rhs(rk, zero);
862 for (std::size_t a = 0; a < rk; ++a)
863 rhs[a] = -C_r[a] / num_traits<T>::from_int(2);
864 const std::vector<T> V_r = line::solve(Fp_r, rhs);
865
866 std::vector<T> xref(model_dim, zero);
867 for (std::size_t i = 0; i < model_dim; ++i) {
868 T s = zero;
869 for (std::size_t a = 0; a < rk; ++a) s += Cinv(i, a) * V_r[a];
870 xref[i] = xss[i] + s / num_traits<T>::from_int(static_cast<long>(n_items));
871 }
872 xss = xref;
873 res.refined = true;
874 } catch (const NumericError&) {
875 // keep the plain mean-field fixed point, as the reference does
876 } catch (const InputError&) {
877 // ditto: a degenerate reduction is not an error of the caller's making
878 }
879
880 res.xss = xss;
881 res.pi0.assign(n_items, zero);
882 for (std::size_t i = 0; i < n_items; ++i) {
883 T v = xss[cache_miss_rmf_index(i, 0, n_items)];
884 if (v < zero) v = zero;
885 if (v > one) v = one;
886 res.pi0[i] = v;
887 }
888 res.MI.assign(n_items, zero);
889 res.M = zero;
890 for (std::size_t i = 0; i < n_items; ++i) {
891 res.MI[i] = lam_i[i] * res.pi0[i];
892 res.M += res.MI[i];
893 }
894 res.MU.assign(u, zero);
895 for (std::size_t v = 0; v < u; ++v) {
896 T s = zero;
897 for (std::size_t i = 0; i < n_items; ++i) s += lambda(v, i) * res.pi0[i];
898 res.MU[v] = s;
899 }
900 return res;
901}
902
903/** cache_miss_rmf on the linear chain, i.e. with no declared access graph. */
904template <class T>
905CacheMissRmfResult<T> cache_miss_rmf(const std::vector<T>& gamma, const std::vector<int>& m_in,
906 const Matrix<T>& lambda, const T& tmax) {
907 return cache_miss_rmf(gamma, m_in, lambda, tmax, std::vector<std::vector<Matrix<T> > >());
908}
909
910/** cache_miss_rmf with the reference horizon tmax = 1e4. */
911template <class T>
912CacheMissRmfResult<T> cache_miss_rmf(const std::vector<T>& gamma, const std::vector<int>& m,
913 const Matrix<T>& lambda) {
914 return cache_miss_rmf(gamma, m, lambda, T(num_traits<T>::from_int(10000)));
915}
916
917/**
918 * Transient mean-field trajectory over [t0,t1] from a given initial occupancy,
919 * the optional TSPAN/X0INIT path of cache_miss_rmf.m. Fills tout, xtraj, pi0_t
920 * and MU_t of the result; the steady-state fields are left at their defaults
921 * because the reference computes them independently of the transient.
922 *
923 * A non-empty `clk` also fills `sojourn`: the clock-weighted average of the
924 * transient, integrated with the drift (`cache_sojourn_ode`), and its per-user
925 * miss rate, which is the weighted average of MU_t because MU_t is affine in x.
926 */
927template <class T>
929 const Matrix<T>& lambda, const T& t0, const T& t1,
930 const std::vector<T>& x0init,
931 const std::vector<std::vector<Matrix<T> > >& accost,
934 "cache_miss_rmf_transient requires transcendental arithmetic");
935 const T zero = num_traits<T>::from_int(0);
936 const T one = num_traits<T>::from_int(1);
937 const std::size_t u = lambda.rows();
938 const std::size_t n_items = lambda.cols();
939 const std::size_t h = m_in.size();
940 const std::size_t model_dim = n_items * (h + 1);
941 if (x0init.size() != model_dim)
942 throw InputError("cache_miss_rmf_transient: initial occupancy has the wrong length");
943
944 std::vector<T> m(h, zero);
945 for (std::size_t k = 0; k < h; ++k) m[k] = num_traits<T>::from_int(static_cast<long>(m_in[k]));
946
947 std::vector<T> lam_i(n_items, zero);
948 T lam_tot = zero;
949 for (std::size_t v = 0; v < u; ++v)
950 for (std::size_t i = 0; i < n_items; ++i) {
951 lam_i[i] += lambda(v, i);
952 lam_tot += lambda(v, i);
953 }
954 std::vector<T> p(n_items, zero);
955 for (std::size_t i = 0; i < n_items; ++i) p[i] = lam_i[i] / lam_tot;
956
958 opt.rtol = num_traits<T>::from_double(1e-8);
959 opt.atol = num_traits<T>::from_double(1e-10);
960 // The transient follows the SAME drift the fixed point is taken on: the
961 // general one under a declared access graph, the chain drift otherwise.
962 const std::vector<Matrix<T> > G = rmf_detail::build_item_graphs(accost, lambda, n_items, h);
963 const auto f = [&](const T& t, const std::vector<T>& x) {
964 (void)t;
965 if (!G.empty()) return rmf_detail::drift_graph(x, p, G, m, n_items, h);
966 return rmf_detail::drift(x, p, m, n_items, h);
967 };
968 const OdeSolution<T> s = ode_rosenbrock4(f, t0, t1, x0init, opt);
969
971 const std::size_t nt = s.t.size();
972 res.tout = s.t;
973 res.xtraj = Matrix<T>(model_dim, nt, zero);
974 for (std::size_t j = 0; j < nt; ++j)
975 for (std::size_t i = 0; i < model_dim; ++i) res.xtraj(i, j) = s.y[j][i];
976 res.pi0_t = Matrix<T>(n_items, nt, zero);
977 for (std::size_t i = 0; i < n_items; ++i)
978 for (std::size_t j = 0; j < nt; ++j) {
979 T v = s.y[j][cache_miss_rmf_index(i, 0, n_items)];
980 if (v < zero) v = zero;
981 if (v > one) v = one;
982 res.pi0_t(i, j) = v;
983 }
984 res.MU_t = Matrix<T>(u, nt, zero);
985 for (std::size_t v = 0; v < u; ++v)
986 for (std::size_t j = 0; j < nt; ++j) {
987 T acc = zero;
988 for (std::size_t i = 0; i < n_items; ++i) acc += lambda(v, i) * res.pi0_t(i, j);
989 res.MU_t(v, j) = acc;
990 }
991 if (!clk.empty()) {
992 res.sojourn = cache_sojourn_ode(f, t0, t1, x0init, clk);
993 if (!res.sojourn.empty()) {
994 res.sojourn.MU.assign(u, zero);
995 for (std::size_t v = 0; v < u; ++v) {
996 T acc = zero;
997 for (std::size_t i = 0; i < n_items; ++i) {
998 T pb = res.sojourn.xbar[cache_miss_rmf_index(i, 0, n_items)];
999 if (pb < zero) pb = zero;
1000 if (pb > one) pb = one;
1001 acc += lambda(v, i) * pb;
1002 }
1003 res.sojourn.MU[v] = acc;
1004 }
1005 }
1006 }
1007 res.M = zero;
1008 return res;
1009}
1010
1011/** cache_miss_rmf_transient on the linear chain, i.e. with no declared access graph. */
1012template <class T>
1014 const Matrix<T>& lambda, const T& t0, const T& t1,
1015 const std::vector<T>& x0init,
1017 return cache_miss_rmf_transient(m_in, lambda, t0, t1, x0init,
1018 std::vector<std::vector<Matrix<T> > >(), clk);
1019}
1020
1021/** Result of `cache_miss_rmf_expansion_transient`. */
1022template <class T>
1024 std::vector<T> t; ///< (n_points) output instants, t[0] = 0
1025 Matrix<T> X; ///< (n_points x model_dim) mean-field trajectory
1026 Matrix<T> V; ///< (n_points x model_dim) 1/N correction trajectory
1027 std::vector<Matrix<T> > W; ///< (n_points) covariance, each model_dim x model_dim
1028};
1029
1030/**
1031 * Refined mean-field TRANSIENT, `CacheRMF.meanFieldExpansionTransient`.
1032 *
1033 * The steady-state refinement solves F' V = -(1/2) sum F''_bc W_bc at the fixed
1034 * point; the transient one carries the same three objects along the trajectory,
1035 * as one coupled system in y = [X (d), V (d), W (d x d)]:
1036 *
1037 * dX/dt = F(X),
1038 * dV/dt = F'(X) V + (1/2) sum_{b,c} F''_{a,b,c} W_{b,c},
1039 * dW/dt = F'(X) W + W F'(X)^T + Q(X),
1040 *
1041 * started from V(0) = 0, W(0) = 0 at the same initial occupancy the fixed point
1042 * uses: the first m(1) items in list 1, the next m(2) in list 2, the rest
1043 * outside. The reported trajectory is X(t) + V(t)/N.
1044 *
1045 * `order = 0` integrates the drift alone and returns V and W identically zero,
1046 * which is the reference's own escape rather than a degenerate case of the
1047 * coupled system.
1048 *
1049 * DOUBLE ONLY, and for the reason the fluid solvers are: the coupled system is
1050 * integrated by LSODA at the reference's own `ode15s` tolerances (RelTol 1e-6,
1051 * AbsTol 1e-10) on the reference's own output grid `linspace(0, time,
1052 * n_points)`, and LSODA's coefficients assume double precision.
1053 *
1054 * THE HESSIAN IS HOISTED OUT OF THE RIGHT-HAND SIDE. The drift is quadratic, so
1055 * F'' does not depend on x -- the reference recomputes it per step, which is
1056 * d^3 work per evaluation for a value that never changes.
1057 */
1058template <class T>
1060 const std::vector<int>& m_in, const Matrix<T>& lambda, const T& time, std::size_t n_points,
1061 int order) {
1062 if (!std::is_same<T, double>::value)
1063 throw UnsupportedError(
1064 "cache_miss_rmf_expansion_transient: the coupled (X,V,W) system is integrated with "
1065 "LSODA, whose coefficients assume double precision; rerun with --arith double");
1066 const T zero = num_traits<T>::from_int(0);
1067 const std::size_t u = lambda.rows();
1068 const std::size_t n_items = lambda.cols();
1069 const std::size_t h = m_in.size();
1070 if (u == 0 || n_items == 0)
1071 throw InputError("cache_miss_rmf_expansion_transient: empty request-rate matrix");
1072 if (h == 0)
1073 throw InputError("cache_miss_rmf_expansion_transient: at least one cache list is required");
1074 if (n_points < 2)
1075 throw InputError("cache_miss_rmf_expansion_transient: at least two output points are "
1076 "required to describe a trajectory");
1077 if (!(num_traits<T>::to_double(time) > 0.0))
1078 throw InputError("cache_miss_rmf_expansion_transient: the horizon must be positive");
1079
1080 std::vector<T> m(h, zero);
1081 for (std::size_t k = 0; k < h; ++k) {
1082 if (m_in[k] <= 0)
1083 throw InputError("cache_miss_rmf_expansion_transient: a list has non-positive capacity");
1084 m[k] = num_traits<T>::from_int(static_cast<long>(m_in[k]));
1085 }
1086
1087 std::vector<T> lam_i(n_items, zero);
1088 T lam_tot = zero;
1089 for (std::size_t v = 0; v < u; ++v)
1090 for (std::size_t i = 0; i < n_items; ++i) {
1091 lam_i[i] += lambda(v, i);
1092 lam_tot += lambda(v, i);
1093 }
1094 if (lam_tot == zero)
1095 throw InputError("cache_miss_rmf_expansion_transient: all request rates are zero");
1096 std::vector<T> p(n_items, zero);
1097 for (std::size_t i = 0; i < n_items; ++i) p[i] = lam_i[i] / lam_tot;
1098
1099 const std::size_t d = n_items * (h + 1);
1100
1101 // The same initial occupancy `cache_miss_rmf` starts its fixed point from.
1102 std::vector<T> x0(d, zero);
1103 {
1104 std::size_t obj = 0;
1105 for (std::size_t k = 1; k <= h; ++k)
1106 for (int jj = 0; jj < m_in[k - 1]; ++jj) {
1107 ++obj;
1108 if (obj <= n_items)
1109 x0[cache_miss_rmf_index(obj - 1, k, n_items)] = num_traits<T>::from_int(1);
1110 }
1111 for (std::size_t i = obj; i < n_items; ++i)
1112 x0[cache_miss_rmf_index(i, 0, n_items)] = num_traits<T>::from_int(1);
1113 }
1114
1115 std::vector<double> grid(n_points, 0.0);
1116 const double tend = num_traits<T>::to_double(time);
1117 for (std::size_t j = 0; j < n_points; ++j)
1118 grid[j] = tend * static_cast<double>(j) / static_cast<double>(n_points - 1);
1119
1120 const std::size_t total = order == 0 ? d : d + d + d * d;
1121 std::vector<double> y0(total, 0.0);
1122 for (std::size_t i = 0; i < d; ++i) y0[i] = num_traits<T>::to_double(x0[i]);
1123
1124 const std::vector<T> Fpp = order == 0 ? std::vector<T>() : rmf_detail::hessian(p, m, n_items, h);
1125
1126 const auto rhs = [&](double t, const double* y, double* dy) {
1127 (void)t;
1128 std::vector<T> x(d, zero);
1129 for (std::size_t i = 0; i < d; ++i) x[i] = num_traits<T>::from_double(y[i]);
1130 const std::vector<T> F = rmf_detail::drift(x, p, m, n_items, h);
1131 for (std::size_t i = 0; i < d; ++i) dy[i] = num_traits<T>::to_double(F[i]);
1132 if (order == 0) return;
1133 const Matrix<T> Fp = rmf_detail::jacobian(x, p, m, n_items, h);
1134 const Matrix<T> Q = rmf_detail::noise_matrix(x, p, m, n_items, h);
1135 for (std::size_t a = 0; a < d; ++a) {
1136 T acc = zero;
1137 for (std::size_t b = 0; b < d; ++b) acc += Fp(a, b) * num_traits<T>::from_double(y[d + b]);
1138 // 0.5 * sum_{b,c} F''_{a,b,c} W_{b,c}
1139 T hcontr = zero;
1140 for (std::size_t b = 0; b < d; ++b)
1141 for (std::size_t c = 0; c < d; ++c) {
1142 const T hv = Fpp[(a * d + b) * d + c];
1143 if (hv == zero) continue;
1144 hcontr += hv * num_traits<T>::from_double(y[2 * d + b * d + c]);
1145 }
1146 dy[d + a] = num_traits<T>::to_double(acc) +
1147 0.5 * num_traits<T>::to_double(hcontr);
1148 }
1149 for (std::size_t a = 0; a < d; ++a)
1150 for (std::size_t b = 0; b < d; ++b) {
1151 T acc = Q(a, b);
1152 for (std::size_t c = 0; c < d; ++c)
1153 acc += Fp(a, c) * num_traits<T>::from_double(y[2 * d + c * d + b]) +
1154 num_traits<T>::from_double(y[2 * d + a * d + c]) * Fp(b, c);
1155 dy[2 * d + a * d + b] = num_traits<T>::to_double(acc);
1156 }
1157 };
1158
1159 LsodaOptions lopt;
1160 lopt.rtol = 1e-6;
1161 lopt.atol = 1e-10;
1162 const LsodaSolution s = lsoda_integrate(rhs, y0, grid, lopt);
1163 if (!s.success || s.y.size() != n_points)
1164 throw NumericError("cache_miss_rmf_expansion_transient: the coupled (X,V,W) system could "
1165 "not be integrated over the requested horizon");
1166
1168 out.t.assign(n_points, zero);
1169 out.X = Matrix<T>(n_points, d, zero);
1170 out.V = Matrix<T>(n_points, d, zero);
1171 out.W.assign(n_points, Matrix<T>(d, d, zero));
1172 for (std::size_t j = 0; j < n_points; ++j) {
1173 out.t[j] = num_traits<T>::from_double(s.t[j]);
1174 for (std::size_t i = 0; i < d; ++i) {
1175 out.X(j, i) = num_traits<T>::from_double(s.y[j][i]);
1176 if (order != 0) out.V(j, i) = num_traits<T>::from_double(s.y[j][d + i]);
1177 }
1178 if (order == 0) continue;
1179 for (std::size_t a = 0; a < d; ++a)
1180 for (std::size_t b = 0; b < d; ++b)
1181 out.W[j](a, b) = num_traits<T>::from_double(s.y[j][2 * d + a * d + b]);
1182 }
1183 return out;
1184}
1185
1186} // namespace cache
1187} // namespace line
1188
1189#endif // LINE_API_CACHE_CACHE_MISS_RMF_H
Grid-independent sojourn-weighted average of a cache mean-field transient.
Malformed or inconsistent input (dimensions, negative populations, ...).
Definition error.h:37
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
The algorithm cannot proceed on this instance (singular matrix, ...).
Definition error.h:43
NumericError(const std::string &what)
Definition error.h:45
UnsupportedError(const std::string &what)
Definition error.h:51
Eigenvalues and singular values, backed by LAPACK.
The exception types the port throws.
Dense linear algebra over the templated number type: products, identity, inverse, and powers.
LSODA: the LINE-facing wrapper over the vendored solver in third_party/lsoda.hpp.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
CacheMissRmfResult< T > cache_miss_rmf(const std::vector< T > &gamma, const std::vector< int > &m_in, const Matrix< T > &lambda, const T &tmax, const std::vector< std::vector< Matrix< T > > > &accost)
Refined mean-field miss rates of a RANDOM(m) multi-list cache.
CacheRmfExpansionTransient< T > cache_miss_rmf_expansion_transient(const std::vector< int > &m_in, const Matrix< T > &lambda, const T &time, std::size_t n_points, int order)
Refined mean-field TRANSIENT, CacheRMF.meanFieldExpansionTransient.
CacheSojournResult< T > cache_sojourn_ode(const F &f, const T &t0, const T &t1, const std::vector< T > &x0, const CacheSojournClock< T > &clk)
Integrates dx/dt = f(t, x) over [t0, t1] from x0 together with clk; see the file comment.
CacheMissRmfResult< T > cache_miss_rmf_transient(const std::vector< int > &m_in, const Matrix< T > &lambda, const T &t0, const T &t1, const std::vector< T > &x0init, const std::vector< std::vector< Matrix< T > > > &accost, const CacheSojournClock< T > &clk=CacheSojournClock< T >())
Transient mean-field trajectory over [t0,t1] from a given initial occupancy, the optional TSPAN/X0INI...
std::size_t cache_miss_rmf_index(std::size_t i, std::size_t k, std::size_t n_items)
Flat index of (item i, list k), k = 0 meaning "not cached" (rmf_index.m).
double dot(const std::vector< double > &a, const std::vector< double > &b)
The inner product of a row vector with a column held as a vector.
Definition mg1.h:240
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
T num_abs(const T &v)
Definition number.h:198
OdeSolution< T > ode_rosenbrock4(const F &f, const J &jac, const T &t0, const T &t1, const std::vector< T > &y0, const OdeOptions< T > &opt)
Integrate y' = f(t,y) from t0 to t1 with an analytic Jacobian.
Definition ode.h:304
LsodaSolution lsoda_integrate(const LsodaRhs &f, const std::vector< double > &y0, const std::vector< double > &t_eval, const LsodaOptions &opt=LsodaOptions())
Definition lsoda.h:178
Matrix< T > inverse(const Matrix< T > &A)
Inverse by LU with one factorization and n back substitutions.
Definition linalg.h:72
Matrix< T > matmul(const Matrix< T > &A, const Matrix< T > &B)
Matrix product A B.
Definition linalg.h:36
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
Number-type abstraction for the templated API port.
Adaptive stiff ODE integrator: a four-stage Rosenbrock method of order four with an embedded order-th...
Integration controls.
Definition lsoda.h:64
double atol
absolute tolerance, applied to every component
Definition lsoda.h:66
double rtol
relative tolerance, applied to every component
Definition lsoda.h:65
Result of an integration, mirroring OdeSolution in ode.h.
Definition lsoda.h:126
bool success
false when LSODA returned istate < 0
Definition lsoda.h:132
std::vector< std::vector< double > > y
y[i] is the state at t[i]
Definition lsoda.h:128
std::vector< double > t
output times, t[0] = t_eval[0]
Definition lsoda.h:127
Integration controls.
Definition ode.h:118
Result of an integration.
Definition ode.h:143
std::vector< std::vector< T > > y
y[i] is the state at t[i]
Definition ode.h:145
std::vector< T > t
accepted time points, t[0] = t0
Definition ode.h:144
Return value of cache_miss_rmf, mirroring [M,MU,MI,pi0,tout,pi0_t,MU_t,xtraj].
std::vector< T > pi0
(n_items) per-item miss probability, clipped to [0,1]
Matrix< T > pi0_t
(n_items x nt) transient miss probability
std::vector< T > tout
transient time grid, empty unless tspan was given
Matrix< T > xtraj
(model_dim x nt) transient occupancy
bool refined
true when the 1/N correction was accepted
Matrix< T > MU_t
(u x nt) transient per-user miss rate
std::vector< T > MI
(n_items) per-item miss rate
CacheSojournResult< T > sojourn
Grid-independent average of the transient against a clock; empty unless one was given.
std::vector< T > xss
the occupancy the metrics were read from
std::vector< T > MU
(u) per-user miss rate
Result of cache_miss_rmf_expansion_transient.
std::vector< Matrix< T > > W
(n_points) covariance, each model_dim x model_dim
Matrix< T > X
(n_points x model_dim) mean-field trajectory
Matrix< T > V
(n_points x model_dim) 1/N correction trajectory
std::vector< T > t
(n_points) output instants, t[0] = 0
A phase-type clock: dphi/dt = phi A, phi(t0) = phi0, density phi c.
A sojourn average: xbar (empty when wtot is not positive), clock mass, per-user miss rate.