Project
Loading...
Searching...
No Matches
CapacityEstimator.cxx
Go to the documentation of this file.
1// Copyright 2019-2020 CERN and copyright holders of ALICE O2.
2// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders.
3// All rights not expressly granted are reserved.
4//
5// This software is distributed under the terms of the GNU General Public
6// License v3 (GPL Version 3), copied verbatim in the file "COPYING".
7//
8// In applying this license CERN does not waive the privileges and immunities
9// granted to it by virtue of its status as an Intergovernmental Organization
10// or submit itself to any jurisdiction.
11
13
14#include <algorithm>
15#include <cassert>
16#include <cmath>
17#include <limits>
18#include <mutex>
19#include <stdexcept>
20#include <tuple>
21#include <unordered_map>
22#include <vector>
23
24#include "Framework/Logger.h"
25
27{
28
30 struct Entry {
31 float ratio{0.f};
32 float margin{0.f};
34 };
35
36 struct UndoRecord {
37 bool existed{false};
39 };
40
41 explicit Impl(Config config) : cfg{config} {}
42
44 mutable std::mutex mutex;
45 std::unordered_map<KeyType, Entry> entries;
46 std::unordered_map<KeyType, UndoRecord> undo;
47 bool transactionActive{false};
48
50 {
51 if (!transactionActive || undo.find(key) != undo.end()) {
52 return;
53 }
54 const auto current = entries.find(key);
55 if (current == entries.end()) {
56 undo.emplace(key, UndoRecord{});
57 } else {
58 undo.emplace(key, UndoRecord{.existed = true, .previous = current->second});
59 }
60 }
61
62 void observe(KeyType key, double scale, size_t requested, size_t granted, size_t emitted,
63 size_t spilled, bool overflowed, bool memoryLimited)
64 {
65 // Record the first-touch undo state before entries[key] can insert or the
66 // existing live entry can be modified. If undo insertion throws, the live
67 // estimator remains unchanged and Tracker's failure path can roll back the
68 // transaction without observing a partial update.
70 auto& e = entries[key];
71 auto& statistics = e.statistics;
72 statistics.requested += requested;
73 statistics.granted += granted;
74 statistics.emitted += emitted;
75 statistics.spilled += spilled;
76
77 const bool firstSample = statistics.samples == 0;
78 if (firstSample) {
79 e.margin = cfg.marginInit;
80 }
81 const auto sample = static_cast<float>(double(emitted) / scale);
82 e.ratio = firstSample ? sample : (cfg.alpha * sample) + ((1.f - cfg.alpha) * e.ratio);
83 statistics.maxEmitted = std::max(statistics.maxEmitted, emitted);
85
86 if (memoryLimited) {
88 e.margin = std::max(cfg.marginMin, e.margin * cfg.marginDown);
89 return;
90 }
91 if (overflowed) {
94 if (!firstSample) {
95 const float shortfall = granted ? static_cast<float>(double(emitted) / double(granted)) : cfg.marginUp;
96 e.margin = std::min(cfg.marginMax, e.margin * std::clamp(shortfall * cfg.marginOverflowSlack, 1.02f, cfg.marginUp));
97 }
98 return;
99 }
100 const float util = granted ? float(double(emitted) / double(granted)) : 1.f;
101 if (util < cfg.lowWatermark) {
103 e.margin = std::max(cfg.marginMin, e.margin * cfg.marginDown);
105 }
106 } else if (statistics.nLowStreak > 0) {
108 }
109 }
110};
111
113
114CapacityEstimator::CapacityEstimator(Config cfg) : mImpl{std::make_unique<Impl>(cfg)} {}
115
117
119{
120 std::lock_guard lock{mImpl->mutex};
121 mImpl->entries.clear();
122 mImpl->undo.clear();
123 mImpl->transactionActive = false;
124}
125
127{
128 std::lock_guard lock{mImpl->mutex};
129 if (mImpl->transactionActive) {
130 throw std::logic_error{"CapacityEstimator transaction already active"};
131 }
132 assert(mImpl->undo.empty());
133 mImpl->transactionActive = true;
134}
135
137{
138 std::lock_guard lock{mImpl->mutex};
139 mImpl->undo.clear();
140 mImpl->transactionActive = false;
141}
142
144{
145 std::lock_guard lock{mImpl->mutex};
146 if (!mImpl->transactionActive) {
147 return;
148 }
149 for (const auto& [key, record] : mImpl->undo) {
150 if (record.existed) {
151 const auto current = mImpl->entries.find(key);
152 assert(current != mImpl->entries.end());
153 current->second = record.previous;
154 } else {
155 mImpl->entries.erase(key);
156 }
157 }
158 mImpl->undo.clear();
159 mImpl->transactionActive = false;
160}
161
162size_t CapacityEstimator::capacity(uint64_t key, double scale) const
163{
164 if (!(scale > 0.)) {
165 return 0;
166 }
167 std::lock_guard lock{mImpl->mutex};
168 const auto it = mImpl->entries.find(key);
169 if (it == mImpl->entries.end() || it->second.statistics.samples == 0) {
170 return mImpl->cfg.floorSlots;
171 }
172 const auto& e = it->second;
173 const double raw = double(e.ratio) * scale * double(e.margin);
174 if (!std::isfinite(raw) || raw < 0.) {
175 return mImpl->cfg.floorSlots;
176 }
177 // A ratio is only meaningful at the scale it was measured at. Learned on a handful of inputs it
178 // can be arbitrarily large, and applying it to a scale orders of magnitude bigger asks for a slab
179 // nobody can allocate. Bound the request by what this site has ever actually emitted: overshooting
180 // burns memory that a bump allocator cannot give back, undershooting only costs one retry.
181 const size_t ceiling = std::max(mImpl->cfg.floorSlots, static_cast<size_t>(double(e.statistics.maxEmitted) * double(mImpl->cfg.marginMax)));
182 if (raw >= static_cast<double>(ceiling)) {
183 return ceiling;
184 }
185 return std::max(mImpl->cfg.floorSlots, static_cast<size_t>(std::ceil(raw)));
186}
187
189{
190 std::lock_guard lock{mImpl->mutex};
191 const auto it = mImpl->entries.find(key);
192 if (it == mImpl->entries.end() || it->second.statistics.maxEmitted == 0) {
193 return mImpl->cfg.floorSlots;
194 }
195 const auto& e = it->second;
196 const double raw = double(e.statistics.maxEmitted) * double(e.margin);
197 if (!std::isfinite(raw) || raw >= static_cast<double>(std::numeric_limits<size_t>::max())) {
198 return std::numeric_limits<size_t>::max();
199 }
200 return std::max(mImpl->cfg.floorSlots, static_cast<size_t>(std::ceil(raw)));
201}
202
203double CapacityEstimator::expected(uint64_t key, double scale) const
204{
205 if (!(scale > 0.)) {
206 return 0.;
207 }
208 std::lock_guard lock{mImpl->mutex};
209 const auto it = mImpl->entries.find(key);
210 if (it == mImpl->entries.end() || it->second.statistics.samples == 0) {
211 return 0.;
212 }
213 const double raw = double(it->second.ratio) * scale;
214 return std::isfinite(raw) && raw > 0. ? raw : 0.;
215}
216
218{
219 std::lock_guard lock{mImpl->mutex};
220 const auto it = mImpl->entries.find(key);
221 if (it == mImpl->entries.end()) {
222 return {};
223 }
224 return it->second.statistics;
225}
226
227void CapacityEstimator::update(uint64_t key, double scale, size_t emitted, size_t capacityUsed, bool overflowed, bool memoryLimited)
228{
229 if (!(scale > 0.)) {
230 return;
231 }
232 std::lock_guard lock{mImpl->mutex};
233 mImpl->observe(key, scale, capacityUsed, capacityUsed, emitted,
234 overflowed && emitted > capacityUsed ? emitted - capacityUsed : 0,
235 overflowed, memoryLimited);
236}
237
238void CapacityEstimator::update(uint64_t key, double scale, size_t requested, size_t granted,
239 size_t emitted, size_t spilled, bool overflowed, bool memoryLimited)
240{
241 if (!(scale > 0.)) {
242 return;
243 }
244 std::lock_guard lock{mImpl->mutex};
245 mImpl->observe(key, scale, requested, granted, emitted, spilled, overflowed, memoryLimited);
246}
247
249{
250 std::lock_guard lock{mImpl->mutex};
251 std::vector<KeyType> keys;
252 keys.reserve(mImpl->entries.size());
253 for (const auto& [key, _] : mImpl->entries) {
254 keys.push_back(key);
255 }
256 std::sort(keys.begin(), keys.end(), [](KeyType a, KeyType b) {
257 const auto da = decodeKey(a);
258 const auto db = decodeKey(b);
259 return std::tie(da.site, da.iteration, da.variant, da.slot) <
260 std::tie(db.site, db.iteration, db.variant, db.slot);
261 });
262 if (keys.empty()) {
263 return;
264 }
265 LOGP(info, "Printing CapacityEstimators:");
266 for (const auto key : keys) {
267 const auto& value = mImpl->entries.at(key);
268 const auto& statistics = value.statistics;
269 const auto decoded = decodeKey(key);
270 LOGP(info, "\tSite:{} | iter:{} | var:({},{}) | slot:{} | ratio:{} | margin:{} | maxEmitted:{} | samples:{} | low:{} | requested:{} | granted:{} | emitted:{} | spilled:{} | overflows:{}", SlabSiteNames[decoded.site], decoded.iteration, getVariantHigh(decoded.variant), getVariantLow(decoded.variant), decoded.slot, value.ratio, value.margin, statistics.maxEmitted, statistics.samples, statistics.nLowStreak, statistics.requested, statistics.granted, statistics.emitted, statistics.spilled, statistics.overflowEvents);
271 }
272}
273
274} // namespace o2::itsmft::tracking
Cross-timeframe output-size prediction.
o2::raw::RawFileWriter * raw
StringRef key
static constexpr int getVariantHigh(int variant) noexcept
Statistics statistics(uint64_t key) const
size_t capacity(uint64_t key, double scale) const
static constexpr int getVariantLow(int variant) noexcept
double expected(uint64_t key, double scale) const
void update(uint64_t key, double scale, size_t emitted, size_t capacityUsed, bool overflowed, bool memoryLimited)
static constexpr Decoded decodeKey(KeyType key) noexcept
GLboolean GLboolean GLboolean b
Definition glcorearb.h:1233
GLsizei const GLfloat * value
Definition glcorearb.h:819
GLboolean GLboolean GLboolean GLboolean a
Definition glcorearb.h:1233
constexpr const char *const SlabSiteNames[SlabSite::NSlabSite]
std::unordered_map< KeyType, Entry > entries
void observe(KeyType key, double scale, size_t requested, size_t granted, size_t emitted, size_t spilled, bool overflowed, bool memoryLimited)
std::unordered_map< KeyType, UndoRecord > undo