LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
fj_tail_ordstat.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_ORDSTAT_H
6#define LINE_API_FJ_FJ_TAIL_ORDSTAT_H
7
8/**
9 * @file
10 * @ingroup api_fj
11 * Tail latency of a k-of-n (QUORUM) fork-join request.
12 *
13 * Templated port of matlab/src/api/fj/fj_tail_ordstat.m, mirrored by
14 * jline.api.fj.FJ_tail_ordstat. The request forks into N parallel tasks and
15 * joins on the KREQ-th of them: KREQ = N is the ordinary AND-join and
16 * reproduces `fj_tail_forktail` EXACTLY, KREQ = 1 is the first completion.
17 *
18 * Each branch is the same black box ForkTail uses -- its task response time is
19 * fitted by a generalized exponential law F_i(x) = (1-exp(-x/beta_i))^alpha_i
20 * matched on the branch mean and variance by `detail::ge_fit` -- and the
21 * request completes once KREQ of the N branches have, so its law is the
22 * KREQ-th ORDER STATISTIC of independent, not identically distributed branch
23 * times,
24 *
25 * F_X(x) = P(at least KREQ of the N branches are done by x),
26 *
27 * the upper tail of a Poisson-binomial with success probabilities F_i(x).
28 * It is evaluated by the convolution recurrence over the branches, whose terms
29 * are all non-negative so it adds no cancellation, and inverted by bisection.
30 *
31 * THE BRACKET IS STRUCTURAL, not searched: X_(KREQ) lies between the FIRST
32 * completion and the LAST, so the smallest single-branch percentile bounds it
33 * below and the AND-join percentile -- which is what `fj_tail_forktail`
34 * returns -- bounds it above.
35 *
36 * At KREQ = N the recurrence reduces TERM BY TERM to prod_i F_i(x), so a full
37 * join evaluates exactly as it did before this header existed; the code returns
38 * the ForkTail root directly there rather than re-deriving it.
39 *
40 * BRANCH INDEPENDENCE is assumed, as in ForkTail: the branches of one request
41 * are positively correlated through their shared arrival instant, so the true
42 * quorum percentile is somewhat larger than this one. The same heavy-traffic
43 * caveat applies, see fj_tail_forktail.h.
44 *
45 * References: M. Nguyen, S. Alesawi, N. Li, H. Che, H. Jiang, "ForkTail: A
46 * Black-Box Fork-Join Tail Latency Prediction Model for User-Facing Datacenter
47 * Workloads", ACM HPDC 2018, for the branch law; A. Thomasian, "Analysis of
48 * Fork/Join and Related Queueing Systems", ACM Computing Surveys 47(2),
49 * Article 17, 2014, Sec. 3, for the quorum.
50 */
51
52#include <cmath>
53#include <cstddef>
54#include <vector>
55
57
58namespace line {
59namespace fj {
60
61namespace detail {
62
63/**
64 * The generalized-exponential CDF (1-exp(-x/beta))^alpha, clamped into [0,1]:
65 * a rounding excursion above one would make the recurrence below emit a
66 * negative complement.
67 */
68template <class T>
69T ge_cdf(const T& x, const T& alpha, const T& beta) {
70 using std::exp;
71 using std::log1p;
72 const T zero = num_traits<T>::from_int(0);
73 const T one = num_traits<T>::from_int(1);
74 if (!(x > zero)) return zero;
75 const T u = exp(alpha * log1p(-exp(-x / beta)));
76 if (!std::isfinite(num_traits<T>::to_double(u))) return zero;
77 if (u < zero) return zero;
78 if (u > one) return one;
79 return u;
80}
81
82/**
83 * P(at least `kreq` successes) for independent Bernoulli trials of success
84 * probabilities `u`, by the convolution recurrence over the trials.
85 */
86template <class T>
87T poissbin_upper(const std::vector<T>& u, std::size_t kreq) {
88 const T zero = num_traits<T>::from_int(0);
89 const T one = num_traits<T>::from_int(1);
90 const std::size_t n = u.size();
91 std::vector<T> pmf(n + 1, zero);
92 pmf[0] = one;
93 for (std::size_t i = 0; i < n; ++i) {
94 for (std::size_t j = i + 1; j >= 1; --j)
95 pmf[j] = T(pmf[j] * (one - u[i]) + pmf[j - 1] * u[i]);
96 pmf[0] = T(pmf[0] * (one - u[i]));
97 }
98 T acc = zero;
99 for (std::size_t j = kreq; j <= n; ++j) acc += pmf[j];
100 return acc;
101}
102
103} // namespace detail
104
105/**
106 * @brief Tail latency of a k-of-n (QUORUM) fork-join request.
107 *
108 * @param ET per-branch mean task response times; one entry means homogeneous
109 * @param VT per-branch variances, same length as ET
110 * @param K fanout, used only when ET has a single entry
111 * @param p_in percentile, a fraction in (0,1) or a percentage in (0,100)
112 * @param kreq the join fires on the kreq-th branch; 0 means every branch
113 */
114template <class T>
115ForkTailResult<T> fj_tail_ordstat(const std::vector<T>& ET, const std::vector<T>& VT,
116 std::size_t K = 1,
117 const T& p_in = num_traits<T>::from_int(99),
118 std::size_t kreq = 0) {
120 "fj_tail_ordstat requires transcendental arithmetic: the generalized "
121 "exponential fit inverts a ratio of digamma and trigamma values");
122 using std::exp;
123 using std::log;
124 using std::log1p;
125 const T zero = num_traits<T>::from_int(0);
126 const T one = num_traits<T>::from_int(1);
127 const T two = num_traits<T>::from_int(2);
128 const T hundred = num_traits<T>::from_int(100);
129
130 if (ET.size() != VT.size())
131 throw InputError("fj_tail_ordstat: ET and VT must have the same number of entries");
132 if (ET.empty()) throw InputError("fj_tail_ordstat: ET must be nonempty");
133
134 T p = p_in;
135 if (p > one) p = T(p / hundred);
136 if (!(p > zero) || !(p < one))
137 throw InputError("fj_tail_ordstat: the percentile must lie strictly between 0 and 1");
138 for (std::size_t i = 0; i < ET.size(); ++i)
139 if (!(ET[i] > zero) || !(VT[i] > zero))
140 throw InputError(
141 "fj_tail_ordstat: the task response time mean and variance must be positive");
142
143 const std::size_t nbranch = ET.size();
144 const std::size_t nsib = (nbranch == 1) ? (K < 1 ? 1 : K) : nbranch;
145 if (kreq == 0) kreq = nsib;
146 if (kreq > nsib)
147 throw InputError("fj_tail_ordstat: the quorum must not exceed the branch count");
148
149 // A homogeneous request over nsib branches is the same order statistic as a
150 // heterogeneous one whose branches all carry the same moments, and expanding
151 // it keeps ONE recurrence instead of a second closed form to keep in step.
152 std::vector<T> et = ET, vt = VT;
153 if (nbranch == 1 && nsib > 1) {
154 et.assign(nsib, ET[0]);
155 vt.assign(nsib, VT[0]);
156 }
157
159 r.alpha.assign(et.size(), zero);
160 r.beta.assign(et.size(), zero);
161 for (std::size_t i = 0; i < et.size(); ++i)
162 detail::ge_fit(et[i], vt[i], r.alpha[i], r.beta[i]);
163
164 // The AND-join percentile bounds every quorum above, and the SMALLEST
165 // single-branch percentile bounds it below.
166 std::vector<T> kv(1, num_traits<T>::from_int(static_cast<long>(nsib)));
167 const ForkTailResult<T> andjoin = fj_tail_forktail<T>(ET, VT, kv, p);
168 if (kreq == nsib) return andjoin;
169 const T xhi = andjoin.xp;
170
171 T xlo = T(-1);
172 for (std::size_t i = 0; i < r.alpha.size(); ++i) {
173 const T cand = -r.beta[i] * log1p(-exp(log(p) / r.alpha[i]));
174 if (!(xlo > zero) || cand < xlo) xlo = cand;
175 }
176
177 auto residual = [&](const T& x) {
178 std::vector<T> u(r.alpha.size(), zero);
179 for (std::size_t i = 0; i < r.alpha.size(); ++i)
180 u[i] = detail::ge_cdf(x, r.alpha[i], r.beta[i]);
181 return T(detail::poissbin_upper(u, kreq) - p);
182 };
183
184 // A single branch may already meet the percentile; the quorum is met earlier.
185 const T tiny = num_traits<T>::from_double(2.220446049250313e-16);
186 while (residual(xlo) > zero && xlo > tiny) xlo = T(xlo / two);
187
188 const RootResult<T> rr =
189 root_brent<T>(residual, xlo, xhi, num_traits<T>::from_double(1e-14) * (one + xhi), 500);
190 r.xp = rr.root;
191 return r;
192}
193
194} // namespace fj
195} // namespace line
196
197#endif // LINE_API_FJ_FJ_TAIL_ORDSTAT_H
InputError(const std::string &what)
Definition error.h:39
ForkTail black-box tail-latency approximation for fork-join requests.
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.
ForkTailResult< T > fj_tail_ordstat(const std::vector< T > &ET, const std::vector< T > &VT, std::size_t K=1, const T &p_in=num_traits< T >::from_int(99), std::size_t kreq=0)
Tail latency of a k-of-n (QUORUM) fork-join request.
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
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