LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
fj_tail_forktail.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_FJ_FJ_TAIL_FORKTAIL_H
6#define LINE_API_FJ_FJ_TAIL_FORKTAIL_H
7
8/**
9 * @file
10 * @ingroup api_fj
11 * ForkTail black-box tail-latency approximation for fork-join requests.
12 *
13 * Templated port of matlab/src/api/fj/fj_tail_forktail.m. No JAR counterpart.
14 * Approximates the p-th percentile of the response time of a request that
15 * forks into K parallel tasks and joins on the last of them, from the mean
16 * and variance of the per-branch task response times ALONE. Each branch is a
17 * black box whose response time is fitted by a generalized exponential law
18 *
19 * F_T(x) = (1 - exp(-x/beta))^alpha,
20 * E[T] = beta (psi(alpha+1) - psi(1)),
21 * V[T] = beta^2 (psi'(1) - psi'(alpha+1)),
22 *
23 * and the request response time is the maximum over the branches, taken as
24 * the PRODUCT of the branch CDFs. That product is exact only for independent
25 * branches, which is the approximation the method rests on.
26 *
27 * Three routes, exactly as in MATLAB:
28 * - homogeneous (one branch mean, scalar K): the product of K identical CDFs
29 * raises the shape to K alpha and inverts in closed form,
30 * x_p = -beta log(1 - p^(1/(K alpha))).
31 * - random fanout (one branch mean, K a vector with probabilities P): the
32 * request law is the mixture sum_i P_i (1-exp(-x/beta))^(K_i alpha),
33 * bracketed between the closed forms at min(K) and max(K) and inverted
34 * numerically.
35 * - heterogeneous (a vector of branch means): solve
36 * sum_i alpha_i log1p(-exp(-x/beta_i)) = log p. F_X is bounded above by
37 * the CDF of any single branch, so the request percentile is at least the
38 * largest branch percentile, which is where the bracket search starts.
39 *
40 * This is a HIGH-LOAD result, from the central limit theorem for G/G/m queues
41 * in heavy traffic: the reference reports errors within 20% and 15% at 80%
42 * and 90% utilization and makes no claim at low load, where the tail is
43 * dominated by the service law and the branch dependence is strongest.
44 *
45 * Reference: M. Nguyen, S. Alesawi, N. Li, H. Che, H. Jiang, "ForkTail: A
46 * Black-Box Fork-Join Tail Latency Prediction Model for User-Facing
47 * Datacenter Workloads", ACM HPDC 2018, pp. 206-217.
48 *
49 * ARITHMETIC: the generalized-exponential fit inverts a ratio of digamma and
50 * trigamma values, so the exact instantiation is refused.
51 */
52
53#include <cmath>
54#include <cstddef>
55#include <vector>
56
58#include "line/num/number.h"
59#include "line/util/error.h"
60#include "line/util/rootfind.h"
61
62namespace line {
63namespace fj {
64
65namespace detail {
66
67/** Recurrence threshold: the Stirling tail is below 1e-17 from here on. */
68inline int digamma_shift() { return 20; }
69
70/**
71 * psi(x) for x > 0, by upward recurrence to x >= 20 then the asymptotic series.
72 * MATLAB's psi(0,x); the recurrence psi(x) = psi(x+1) - 1/x is exact, so the
73 * only error is the truncation of the Stirling tail. The threshold is 20 and
74 * not the more usual 6 or 8 because the first omitted term is B12/(12 x^12),
75 * which is still 3e-13 at x = 8 and would cap the fit at six correct digits.
76 */
77template <class T>
78T digamma(const T& x0) {
79 const T one = num_traits<T>::from_int(1);
80 const T lim = num_traits<T>::from_int(digamma_shift());
81 T x = x0, acc = num_traits<T>::from_int(0);
82 while (x < lim) {
83 acc -= one / x;
84 x += one;
85 }
86 const T inv = one / x, inv2 = inv * inv;
87 using std::log;
88 T s = log(x) - inv / num_traits<T>::from_int(2);
89 T p = inv2;
90 s -= p / num_traits<T>::from_int(12);
91 p *= inv2;
92 s += p / num_traits<T>::from_int(120);
93 p *= inv2;
94 s -= p / num_traits<T>::from_int(252);
95 p *= inv2;
96 s += p / num_traits<T>::from_int(240);
97 p *= inv2;
98 s -= p / num_traits<T>::from_int(132);
99 p *= inv2;
100 s += p * num_traits<T>::from_int(691) / num_traits<T>::from_int(32760);
101 return acc + s;
102}
103
104/** psi'(x) for x > 0, the same construction on psi'(x) = psi'(x+1) + 1/x^2. */
105template <class T>
106T trigamma(const T& x0) {
107 const T one = num_traits<T>::from_int(1);
108 const T lim = num_traits<T>::from_int(digamma_shift());
109 T x = x0, acc = num_traits<T>::from_int(0);
110 while (x < lim) {
111 acc += one / (x * x);
112 x += one;
113 }
114 const T inv = one / x, inv2 = inv * inv;
115 T s = inv + inv2 / num_traits<T>::from_int(2);
116 T p = inv2 * inv;
117 s += p / num_traits<T>::from_int(6);
118 p *= inv2;
119 s -= p / num_traits<T>::from_int(30);
120 p *= inv2;
121 s += p / num_traits<T>::from_int(42);
122 p *= inv2;
123 s -= p / num_traits<T>::from_int(30);
124 p *= inv2;
125 s += p * num_traits<T>::from_int(5) / num_traits<T>::from_int(66);
126 return acc + s;
127}
128
129/**
130 * Match a generalized exponential on a mean and a variance.
131 *
132 * The squared coefficient of variation depends on the SHAPE alone and
133 * decreases monotonically in it, so the shape is recovered by a scalar root
134 * find on a logarithmic scale and the scale then follows in closed form.
135 * SCV = 1 is the exponential case alpha = 1 and is kept exact.
136 */
137template <class T>
138void ge_fit(const T& ET, const T& VT, T& alpha, T& beta) {
139 const T one = num_traits<T>::from_int(1);
140 const T scv = VT / (ET * ET);
141 const T fine = num_traits<T>::from_double(1e-12);
142 if (num_abs(T(scv - one)) < fine) {
143 alpha = one;
144 } else {
145 const T tri1 = trigamma(one);
146 const T di1 = digamma(one);
147 auto residual = [&](const T& la) {
148 using std::exp;
149 const T a = exp(la);
150 const T d = digamma(T(a + one)) - di1;
151 return T((tri1 - trigamma(T(a + one))) / (d * d) - scv);
152 };
153 T lo = num_traits<T>::from_int(-30), hi = num_traits<T>::from_int(30);
154 const T lomin = num_traits<T>::from_int(-700), himax = num_traits<T>::from_int(700);
155 const T step = num_traits<T>::from_int(30);
156 const T zero = num_traits<T>::from_int(0);
157 while (residual(lo) < zero && lo > lomin) lo -= step; // smaller shape, larger SCV
158 while (residual(hi) > zero && hi < himax) hi += step; // larger shape, smaller SCV
159 const RootResult<T> rr =
160 root_brent<T>(residual, lo, hi, num_traits<T>::from_double(1e-14), 500);
161 using std::exp;
162 alpha = exp(rr.root);
163 }
164 beta = ET / (digamma(T(alpha + one)) - digamma(one));
165}
166
167} // namespace detail
168
169/** Mirrors MATLAB's [xp, alpha, beta] return list. */
170template <class T>
172 T xp; ///< predicted p-th percentile of the request response time
173 std::vector<T> alpha; ///< fitted shape parameters, one per branch
174 std::vector<T> beta; ///< fitted scale parameters, one per branch
175};
176
177/**
178 * @brief ForkTail black-box tail-latency approximation for fork-join
179 * requests.
180 *
181 * @param ET per-branch mean task response times; one entry means homogeneous
182 * @param VT per-branch variances, same length as ET
183 * @param K fanout(s); a vector of distinct fanouts needs P, ignored when ET
184 * has more than one entry
185 * @param P fanout probabilities, required when K has more than one entry
186 * @param p_in percentile, a fraction in (0,1) or a percentage in (0,100)
187 */
188template <class T>
189ForkTailResult<T> fj_tail_forktail(const std::vector<T>& ET, const std::vector<T>& VT,
190 const std::vector<T>& K = std::vector<T>(),
191 const T& p_in = num_traits<T>::from_int(99),
192 const std::vector<T>& P = std::vector<T>()) {
194 "fj_tail_forktail requires transcendental arithmetic: the generalized "
195 "exponential fit inverts a ratio of digamma and trigamma values");
196 using std::exp;
197 using std::log;
198 using std::log1p;
199 const T zero = num_traits<T>::from_int(0);
200 const T one = num_traits<T>::from_int(1);
201 const T two = num_traits<T>::from_int(2);
202 const T hundred = num_traits<T>::from_int(100);
203
204 std::vector<T> Kv = K;
205 if (Kv.empty()) Kv.assign(1, one);
206 T p = p_in;
207 if (p > one) p = p / hundred;
208 if (p <= zero || p >= one)
209 throw InputError("fj_tail_forktail: the percentile must lie strictly between 0 and 1");
210 if (VT.size() != ET.size())
211 throw InputError("fj_tail_forktail: ET and VT must have the same number of entries");
212 if (ET.empty()) throw InputError("fj_tail_forktail: ET must be nonempty");
213 for (std::size_t i = 0; i < ET.size(); ++i)
214 if (ET[i] <= zero || VT[i] <= zero)
215 throw InputError(
216 "fj_tail_forktail: the task response time mean and variance must be positive");
217
218 const std::size_t nbranch = ET.size();
220 r.alpha.assign(nbranch, zero);
221 r.beta.assign(nbranch, zero);
222 for (std::size_t i = 0; i < nbranch; ++i) detail::ge_fit(ET[i], VT[i], r.alpha[i], r.beta[i]);
223
224 if (nbranch == 1 && Kv.size() > 1) {
225 // random fanout: mix the homogeneous request laws and invert numerically
226 if (P.size() != Kv.size())
227 throw InputError(
228 "fj_tail_forktail: a vector of fanouts K needs a probability vector P of the "
229 "same length");
230 T sp = zero;
231 for (std::size_t i = 0; i < P.size(); ++i) {
232 if (P[i] < zero)
233 throw InputError("fj_tail_forktail: the fanout probabilities P must be "
234 "non-negative and sum to one");
235 sp += P[i];
236 }
237 if (num_abs(T(sp - one)) > num_traits<T>::from_double(1e-6))
238 throw InputError(
239 "fj_tail_forktail: the fanout probabilities P must be non-negative and sum to one");
240 const T alpha = r.alpha[0], beta = r.beta[0];
241 T kmin = Kv[0], kmax = Kv[0];
242 for (std::size_t i = 1; i < Kv.size(); ++i) {
243 if (Kv[i] < kmin) kmin = Kv[i];
244 if (Kv[i] > kmax) kmax = Kv[i];
245 }
246 auto mixres = [&](const T& x) {
247 T s = zero;
248 for (std::size_t i = 0; i < Kv.size(); ++i)
249 s += P[i] * exp(Kv[i] * alpha * log1p(-exp(-x / beta)));
250 return T(s - p);
251 };
252 const T xlo = -beta * log1p(-exp(log(p) / (kmin * alpha)));
253 const T xhi = -beta * log1p(-exp(log(p) / (kmax * alpha)));
254 const T lo = xlo < xhi ? xlo : xhi;
255 const T hi = xlo < xhi ? xhi : xlo;
256 if (lo == hi) {
257 r.xp = lo;
258 return r;
259 }
260 // G(x)^(K alpha) decreases in K, so the mixture obeys mixres(lo) <= 0
261 // <= mixres(hi) exactly -- with EQUALITY when P puts all its mass on
262 // kmin or on kmax. There the root sits ON an endpoint, the residual
263 // there is roundoff of either sign rather than the strict straddle
264 // root_brent demands, and the endpoint is already the answer.
265 const T flo = mixres(lo);
266 const T fhi = mixres(hi);
267 if (!(flo < zero)) {
268 r.xp = lo;
269 return r;
270 }
271 if (!(fhi > zero)) {
272 r.xp = hi;
273 return r;
274 }
275 const RootResult<T> rr = root_brent<T>(
276 mixres, lo, hi, num_traits<T>::from_double(1e-14) * (one + hi), 500);
277 r.xp = rr.root;
278 return r;
279 }
280
281 if (nbranch == 1) {
282 // homogeneous: K identical CDFs raise the shape to K alpha
283 r.xp = -r.beta[0] * log1p(-exp(log(p) / (Kv[0] * r.alpha[0])));
284 return r;
285 }
286
287 // heterogeneous: solve prod_i (1-exp(-x/beta_i))^alpha_i = p on the log scale
288 const T logp = log(p);
289 auto residual = [&](const T& x) {
290 T s = zero;
291 for (std::size_t i = 0; i < nbranch; ++i)
292 s += r.alpha[i] * log1p(-exp(-x / r.beta[i]));
293 return T(s - logp);
294 };
295 T xlo = zero;
296 for (std::size_t i = 0; i < nbranch; ++i) {
297 const T cand = -r.beta[i] * log1p(-exp(logp / r.alpha[i]));
298 if (cand > xlo) xlo = cand;
299 }
300 T xhi = xlo;
301 while (residual(xhi) < zero) {
302 xhi = two * xhi;
303 if (!std::isfinite(num_traits<T>::to_double(xhi)))
304 throw InputError("fj_tail_forktail: could not bracket the ForkTail percentile");
305 }
306 const T tiny = num_traits<T>::from_double(2.220446049250313e-16);
307 if (residual(xlo) > zero) {
308 xlo = xlo / two;
309 while (residual(xlo) > zero && xlo > tiny) xlo = xlo / two;
310 }
311 const RootResult<T> rr =
312 root_brent<T>(residual, xlo, xhi, num_traits<T>::from_double(1e-14) * (one + xhi), 500);
313 r.xp = rr.root;
314 return r;
315}
316
317/** Homogeneous convenience overload with a scalar mean, variance and fanout. */
318template <class T>
319ForkTailResult<T> fj_tail_forktail(const T& ET, const T& VT, const T& K,
320 const T& p = num_traits<T>::from_int(99)) {
321 return fj_tail_forktail(std::vector<T>(1, ET), std::vector<T>(1, VT), std::vector<T>(1, K), p);
322}
323
324} // namespace fj
325} // namespace line
326
327#endif // LINE_API_FJ_FJ_TAIL_FORKTAIL_H
InputError(const std::string &what)
Definition error.h:39
The exception types the port throws.
Shared return types and arithmetic helpers for the templated fork-join port.
ForkTailResult< T > fj_tail_forktail(const std::vector< T > &ET, const std::vector< T > &VT, const std::vector< T > &K=std::vector< T >(), const T &p_in=num_traits< T >::from_int(99), const std::vector< T > &P=std::vector< T >())
ForkTail black-box tail-latency approximation for fork-join requests.
T num_abs(const T &v)
Definition number.h:172
RootResult< T > root_brent(F f, const T &a0, const T &b0, const T &tol, unsigned maxiter=200)
Brent's method on a bracket with a sign change.
Definition rootfind.h:130
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
Mirrors MATLAB's [xp, alpha, beta] return list.
std::vector< T > beta
fitted scale parameters, one per branch
T xp
predicted p-th percentile of the request response time
std::vector< T > alpha
fitted shape parameters, one per branch