733 lines
32 KiB
C++
733 lines
32 KiB
C++
#pragma once
|
|
|
|
#include "universal_container/tiered_storage.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <bit>
|
|
#include <cmath>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <limits>
|
|
#include <optional>
|
|
#include <utility>
|
|
|
|
namespace uc {
|
|
|
|
enum class StorageMode : std::uint8_t {
|
|
vector,
|
|
tiered
|
|
};
|
|
|
|
enum class ResidencyMode : std::uint8_t {
|
|
automatic,
|
|
forced_vector,
|
|
forced_tiered
|
|
};
|
|
|
|
// Deferred is the safe default: reads collect statistics, but a representation
|
|
// change is performed only at a mutation boundary or by adapt_now(). Eager is
|
|
// retained as an explicitly unsafe experiment for non-const reads.
|
|
enum class ReadAdaptationMode : std::uint8_t {
|
|
deferred,
|
|
eager_nonconst
|
|
};
|
|
|
|
enum class OperationKind : std::uint8_t {
|
|
random_read,
|
|
sequential_read,
|
|
append,
|
|
insert,
|
|
erase,
|
|
set
|
|
};
|
|
|
|
struct OperationSample {
|
|
OperationKind kind = OperationKind::random_read;
|
|
std::size_t size = 0;
|
|
std::size_t position = 0;
|
|
std::size_t count = 1;
|
|
std::size_t element_bytes = 4;
|
|
};
|
|
|
|
struct AdaptationDecision {
|
|
StorageMode target = StorageMode::vector;
|
|
TieredConfig tiered_config{};
|
|
double expected_saving = 0.0;
|
|
};
|
|
|
|
struct AdaptationConfig {
|
|
// Selected by the paired aggressive/moderate/balanced benchmark: this
|
|
// reacts early in a sustained edit phase but leaves a 128-edit negative
|
|
// control in vector mode.
|
|
std::size_t evaluation_interval = 256;
|
|
std::size_t minimum_observations = 2048;
|
|
std::size_t edit_evaluation_interval = 32;
|
|
std::size_t minimum_edit_observations = 128;
|
|
std::size_t minimum_tiered_size = 8 * 1024;
|
|
// The forecast is deliberately bounded by both evidence accumulated in
|
|
// the current residency and this hard ceiling. Treating the ceiling as a
|
|
// guaranteed future phase length makes short edit bursts look much more
|
|
// profitable than they really are.
|
|
std::size_t forecast_operations = 128 * 1024;
|
|
std::size_t minimum_forecast_operations = 256;
|
|
double forecast_growth_factor = 8.0;
|
|
// A sampled read carries `count = read_sample_rate`, so the statistical
|
|
// weight is preserved while the hot operator[] path pays the full policy
|
|
// update only once per cache-line-sized batch of accesses.
|
|
std::size_t read_sample_rate = 256;
|
|
// Structural samples retain their aggregate weight. Sampling every edit
|
|
// made the replaceable policy itself a visible part of edit latency.
|
|
std::size_t edit_sample_rate = 4;
|
|
std::size_t minimum_residency_operations = 256;
|
|
// Storage-mode hysteresis uses the edit share of the workload EWMA. The
|
|
// wider entry threshold and narrower exit threshold form a Schmitt trigger:
|
|
// a tiered sequence is not converted back to vector while edits are still
|
|
// a material part of the current phase.
|
|
double tiered_entry_edit_fraction = 0.05;
|
|
double tiered_exit_edit_fraction = 0.01;
|
|
std::size_t required_confirmation_windows = 2;
|
|
// Shape changes are independent from storage-mode changes. They need a
|
|
// longer residency and an edit-heavy phase because changing leaf capacity
|
|
// is itself a full O(N) rebuild.
|
|
std::size_t minimum_shape_residency_operations = 4 * 1024;
|
|
double minimum_shape_edit_fraction = 0.10;
|
|
std::size_t locality_radius_divisor = 50;
|
|
std::size_t maximum_directory_levels = 4;
|
|
double vector_to_tiered_safety = 1.50;
|
|
double tiered_to_vector_safety = 2.00;
|
|
double tiered_rebuild_safety = 2.50;
|
|
// A leaf rebuild needs a material advantage over the incumbent, not merely
|
|
// to win a noisy window. Directory-only changes still have their much
|
|
// smaller rebuild cost, but use the same relative stability guard.
|
|
double minimum_shape_improvement = 0.15;
|
|
double ewma_alpha = 0.35;
|
|
|
|
// The search space is data, not a hard-coded branch in the container. A
|
|
// different policy/config can replace it without touching storage code.
|
|
std::array<std::size_t, 5> tiered_leaf_candidates{64, 128, 256, 512, 1024};
|
|
|
|
// Calibratable cost units. The leaf movement term grows with B, while the
|
|
// directory/fragmentation term grows with N/B. Their intersection is why
|
|
// the selected block size changes with both N and edit locality.
|
|
double vector_read = 1.0;
|
|
double tiered_read_base = 12.0;
|
|
double tiered_read_per_level = 2.0;
|
|
double tiered_lookup_per_log_top = 1.20;
|
|
double vector_scan_element = 0.70;
|
|
double tiered_scan_element = 0.92;
|
|
double tiered_scan_boundary = 12.0;
|
|
double vector_append = 2.0;
|
|
double tiered_append = 2.8;
|
|
double vector_edit_fixed = 6.0;
|
|
double tiered_edit_fixed = 30.0;
|
|
double vector_move_unit = 0.025;
|
|
double tiered_move_unit = 4.0;
|
|
double tiered_directory_unit = 0.65;
|
|
double localized_directory_multiplier = 0.15;
|
|
double conversion_unit_per_element = 1.0;
|
|
double directory_rebuild_unit_per_leaf = 8.0;
|
|
};
|
|
|
|
struct AdaptationTelemetry {
|
|
std::uint64_t observed_operations = 0;
|
|
std::uint64_t sampled_reads = 0;
|
|
std::uint64_t evaluations = 0;
|
|
std::uint64_t vector_to_tiered = 0;
|
|
std::uint64_t tiered_to_vector = 0;
|
|
std::uint64_t tiered_rebuilds = 0;
|
|
std::uint64_t tiered_leaf_rebuilds = 0;
|
|
std::uint64_t tiered_directory_rebuilds = 0;
|
|
std::size_t last_recommended_leaf = 0;
|
|
std::size_t last_recommended_levels = 0;
|
|
double last_vector_cost_per_operation = 0.0;
|
|
double last_tiered_cost_per_operation = 0.0;
|
|
double last_current_cost_per_operation = 0.0;
|
|
double last_expected_saving = 0.0;
|
|
double last_conversion_cost = 0.0;
|
|
double last_local_edit_fraction = 0.0;
|
|
double last_edit_fraction = 0.0;
|
|
std::size_t last_forecast_operations = 0;
|
|
std::size_t last_evidence_windows = 0;
|
|
};
|
|
|
|
class CostModelPolicy {
|
|
public:
|
|
static constexpr std::size_t candidate_count = 5;
|
|
|
|
explicit CostModelPolicy(AdaptationConfig config = {}, TieredConfig tiered = {})
|
|
: config_(normalize(config)), active_tiered_(normalize_tiered(tiered)),
|
|
search_fanout_(active_tiered_.directory_fanout),
|
|
search_max_levels_(config_.maximum_directory_levels) {}
|
|
|
|
void set_tiered_config(TieredConfig tiered) noexcept {
|
|
active_tiered_ = normalize_tiered(tiered);
|
|
search_fanout_ = active_tiered_.directory_fanout;
|
|
search_max_levels_ = config_.maximum_directory_levels;
|
|
}
|
|
|
|
void observe(const OperationSample& sample) noexcept {
|
|
const auto count = static_cast<double>(std::max<std::size_t>(1, sample.count));
|
|
last_element_bytes_ = std::max<std::size_t>(1, sample.element_bytes);
|
|
last_observed_size_ = sample.size;
|
|
operations_since_transition_ += sample.count;
|
|
operations_since_shape_transition_ += sample.count;
|
|
telemetry_.observed_operations += sample.count;
|
|
if (sample.kind == OperationKind::random_read) {
|
|
++telemetry_.sampled_reads;
|
|
}
|
|
|
|
const bool edit = is_edit(sample.kind);
|
|
// In vector mode, reads/appends cannot make tiered storage preferable.
|
|
// Before the first relevant edit (or below the measured small-size
|
|
// threshold), avoid evaluating all five counterfactual shapes in the
|
|
// hot path. Once edits exist, sampled reads are evaluated normally so
|
|
// that a short burst can be rejected rather than converted later.
|
|
if (active_mode_ == StorageMode::vector
|
|
&& (sample.size < config_.minimum_tiered_size
|
|
|| (!edit && total_edit_weight_ == 0.0
|
|
&& window_edit_weight_ == 0.0
|
|
&& !decision_ready_ && !pending_decision_))) {
|
|
return;
|
|
}
|
|
|
|
const bool localized = classify_edit_locality(sample);
|
|
|
|
window_vector_cost_ += estimate_vector(sample);
|
|
window_active_tiered_cost_ += estimate_tiered(sample, active_tiered_, localized);
|
|
for (std::size_t i = 0; i < candidate_count; ++i) {
|
|
window_candidate_costs_[i] += estimate_tiered(
|
|
sample, candidate_config(i, sample.size), localized);
|
|
}
|
|
window_weight_ += count;
|
|
if (edit) {
|
|
window_edit_weight_ += count;
|
|
total_edit_weight_ += count;
|
|
if (localized) {
|
|
window_local_edit_weight_ += count;
|
|
}
|
|
}
|
|
|
|
if (window_weight_ >= static_cast<double>(config_.evaluation_interval)
|
|
|| window_edit_weight_ >= static_cast<double>(
|
|
config_.edit_evaluation_interval)) {
|
|
close_window();
|
|
}
|
|
}
|
|
|
|
[[nodiscard]] bool decision_ready() const noexcept { return decision_ready_; }
|
|
|
|
[[nodiscard]] std::optional<AdaptationDecision>
|
|
recommended_decision(StorageMode current,
|
|
std::size_t size,
|
|
TieredConfig current_config) noexcept {
|
|
active_mode_ = current;
|
|
if (!decision_ready_) {
|
|
return std::nullopt;
|
|
}
|
|
const auto evidence_windows = closed_windows_since_decision_;
|
|
decision_ready_ = false;
|
|
closed_windows_since_decision_ = 0;
|
|
++telemetry_.evaluations;
|
|
telemetry_.last_evidence_windows = evidence_windows;
|
|
const auto evaluation = evaluate_decision(current, size, current_config);
|
|
telemetry_.last_vector_cost_per_operation = evaluation.vector_cost;
|
|
telemetry_.last_tiered_cost_per_operation = evaluation.best_tiered_cost;
|
|
telemetry_.last_current_cost_per_operation = evaluation.current_cost;
|
|
telemetry_.last_recommended_leaf = evaluation.best_config.leaf_capacity;
|
|
telemetry_.last_recommended_levels = evaluation.best_config.directory_levels;
|
|
telemetry_.last_conversion_cost = evaluation.selected_rebuild_cost;
|
|
telemetry_.last_expected_saving = evaluation.decision
|
|
? evaluation.decision->expected_saving : 0.0;
|
|
telemetry_.last_forecast_operations = evaluation.forecast_operations;
|
|
telemetry_.last_edit_fraction = ewma_edit_fraction_;
|
|
|
|
return confirm(evaluation.decision);
|
|
}
|
|
|
|
// Compatibility with simpler storage wrappers: this intentionally loses a
|
|
// same-mode tiered shape recommendation.
|
|
[[nodiscard]] std::optional<StorageMode>
|
|
recommended_mode(StorageMode current, std::size_t size) noexcept {
|
|
const auto decision = recommended_decision(current, size, active_tiered_);
|
|
return decision ? std::optional<StorageMode>(decision->target) : std::nullopt;
|
|
}
|
|
|
|
void on_transition(StorageMode from,
|
|
StorageMode to,
|
|
TieredConfig active_config) noexcept {
|
|
if (from == StorageMode::vector && to == StorageMode::tiered) {
|
|
++telemetry_.vector_to_tiered;
|
|
} else if (from == StorageMode::tiered && to == StorageMode::vector) {
|
|
++telemetry_.tiered_to_vector;
|
|
} else if (from == StorageMode::tiered && to == StorageMode::tiered) {
|
|
++telemetry_.tiered_rebuilds;
|
|
if (normalize_tiered(active_config).leaf_capacity
|
|
== active_tiered_.leaf_capacity) {
|
|
++telemetry_.tiered_directory_rebuilds;
|
|
} else {
|
|
++telemetry_.tiered_leaf_rebuilds;
|
|
}
|
|
}
|
|
const auto normalized_active = normalize_tiered(active_config);
|
|
if (to == StorageMode::tiered) {
|
|
synchronize_active_tiered_ewma(normalized_active);
|
|
}
|
|
active_tiered_ = normalized_active;
|
|
active_mode_ = to;
|
|
clear_transition_evidence();
|
|
}
|
|
|
|
void on_transition(StorageMode from, StorageMode to) noexcept {
|
|
on_transition(from, to, active_tiered_);
|
|
}
|
|
|
|
void reset() noexcept {
|
|
const auto active = active_tiered_;
|
|
const auto mode = active_mode_;
|
|
const auto fanout = search_fanout_;
|
|
const auto levels = search_max_levels_;
|
|
*this = CostModelPolicy(config_, active);
|
|
active_mode_ = mode;
|
|
search_fanout_ = fanout;
|
|
search_max_levels_ = levels;
|
|
}
|
|
|
|
[[nodiscard]] const AdaptationConfig& config() const noexcept { return config_; }
|
|
[[nodiscard]] const AdaptationTelemetry& telemetry() const noexcept { return telemetry_; }
|
|
|
|
private:
|
|
static bool is_edit(OperationKind kind) noexcept {
|
|
return kind == OperationKind::insert || kind == OperationKind::erase;
|
|
}
|
|
|
|
static TieredConfig normalize_tiered(TieredConfig config) noexcept {
|
|
config.leaf_capacity = std::clamp<std::size_t>(config.leaf_capacity, 4, 1U << 20U);
|
|
config.directory_fanout = std::clamp<std::size_t>(
|
|
config.directory_fanout, 2, 1U << 16U);
|
|
config.directory_levels = std::clamp<std::size_t>(config.directory_levels, 1, 8);
|
|
return config;
|
|
}
|
|
|
|
static AdaptationConfig normalize(AdaptationConfig config) noexcept {
|
|
config.evaluation_interval = std::max<std::size_t>(16, config.evaluation_interval);
|
|
config.minimum_observations = std::max(config.evaluation_interval,
|
|
config.minimum_observations);
|
|
config.edit_evaluation_interval = std::max<std::size_t>(8,
|
|
config.edit_evaluation_interval);
|
|
config.minimum_edit_observations = std::max(
|
|
config.edit_evaluation_interval, config.minimum_edit_observations);
|
|
config.forecast_operations = std::max<std::size_t>(1, config.forecast_operations);
|
|
config.minimum_forecast_operations = std::clamp<std::size_t>(
|
|
config.minimum_forecast_operations, 1, config.forecast_operations);
|
|
config.forecast_growth_factor = std::max(1.0, config.forecast_growth_factor);
|
|
config.read_sample_rate = std::max<std::size_t>(1, config.read_sample_rate);
|
|
config.edit_sample_rate = std::max<std::size_t>(1, config.edit_sample_rate);
|
|
config.tiered_entry_edit_fraction = std::clamp(
|
|
config.tiered_entry_edit_fraction, 0.0, 1.0);
|
|
config.tiered_exit_edit_fraction = std::clamp(
|
|
config.tiered_exit_edit_fraction, 0.0,
|
|
config.tiered_entry_edit_fraction);
|
|
config.required_confirmation_windows = std::max<std::size_t>(
|
|
1, config.required_confirmation_windows);
|
|
config.minimum_shape_edit_fraction = std::clamp(
|
|
config.minimum_shape_edit_fraction, 0.0, 1.0);
|
|
config.locality_radius_divisor = std::max<std::size_t>(1,
|
|
config.locality_radius_divisor);
|
|
config.maximum_directory_levels = std::clamp<std::size_t>(
|
|
config.maximum_directory_levels, 1, 8);
|
|
config.minimum_shape_improvement = std::clamp(
|
|
config.minimum_shape_improvement, 0.0, 1.0);
|
|
config.ewma_alpha = std::clamp(config.ewma_alpha, 0.01, 1.0);
|
|
for (auto& leaf : config.tiered_leaf_candidates) {
|
|
leaf = std::clamp<std::size_t>(leaf, 4, 1U << 20U);
|
|
}
|
|
std::sort(config.tiered_leaf_candidates.begin(),
|
|
config.tiered_leaf_candidates.end());
|
|
return config;
|
|
}
|
|
|
|
[[nodiscard]] std::size_t actual_levels(std::size_t size,
|
|
std::size_t leaf,
|
|
std::size_t fanout,
|
|
std::size_t maximum) const noexcept {
|
|
auto nodes = (size + leaf - 1) / leaf;
|
|
std::size_t levels = 1;
|
|
while (nodes > fanout && levels < maximum) {
|
|
nodes = (nodes + fanout - 1) / fanout;
|
|
++levels;
|
|
}
|
|
return levels;
|
|
}
|
|
|
|
[[nodiscard]] TieredConfig candidate_config(std::size_t index,
|
|
std::size_t size) const noexcept {
|
|
const auto leaf = config_.tiered_leaf_candidates[index];
|
|
return {leaf, search_fanout_,
|
|
actual_levels(size, leaf, search_fanout_, search_max_levels_)};
|
|
}
|
|
|
|
bool classify_edit_locality(const OperationSample& sample) noexcept {
|
|
if (!is_edit(sample.kind)) {
|
|
return false;
|
|
}
|
|
bool localized = false;
|
|
if (has_last_edit_) {
|
|
const auto distance = sample.position > last_edit_position_
|
|
? sample.position - last_edit_position_
|
|
: last_edit_position_ - sample.position;
|
|
const auto radius = std::max<std::size_t>(
|
|
32, sample.size / config_.locality_radius_divisor);
|
|
localized = distance <= radius;
|
|
}
|
|
last_edit_position_ = sample.position;
|
|
has_last_edit_ = true;
|
|
return localized;
|
|
}
|
|
|
|
[[nodiscard]] double estimate_vector(const OperationSample& sample) const noexcept {
|
|
const auto count = static_cast<double>(std::max<std::size_t>(1, sample.count));
|
|
switch (sample.kind) {
|
|
case OperationKind::random_read:
|
|
return config_.vector_read * count;
|
|
case OperationKind::sequential_read:
|
|
return config_.vector_scan_element * count;
|
|
case OperationKind::append:
|
|
return config_.vector_append * count;
|
|
case OperationKind::set:
|
|
return config_.vector_read * 1.3 * count;
|
|
case OperationKind::insert:
|
|
case OperationKind::erase: {
|
|
const auto width_scale = std::max(1.0,
|
|
static_cast<double>(sample.element_bytes) / sizeof(std::uint32_t));
|
|
const auto tail = sample.position < sample.size
|
|
? sample.size - sample.position : 0;
|
|
return (config_.vector_edit_fixed
|
|
+ config_.vector_move_unit * width_scale * static_cast<double>(tail))
|
|
* count;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
[[nodiscard]] double estimate_tiered(const OperationSample& sample,
|
|
TieredConfig tiered,
|
|
bool localized) const noexcept {
|
|
tiered = normalize_tiered(tiered);
|
|
const auto count = static_cast<double>(std::max<std::size_t>(1, sample.count));
|
|
const auto leaf_count = std::max<std::size_t>(
|
|
1, (sample.size + tiered.leaf_capacity - 1) / tiered.leaf_capacity);
|
|
const auto level_count = actual_levels(
|
|
sample.size, tiered.leaf_capacity, tiered.directory_fanout,
|
|
tiered.directory_levels);
|
|
auto top_nodes = leaf_count;
|
|
for (std::size_t level = 1; level < level_count; ++level) {
|
|
top_nodes = (top_nodes + tiered.directory_fanout - 1)
|
|
/ tiered.directory_fanout;
|
|
}
|
|
const auto levels = static_cast<double>(level_count);
|
|
const auto top_log2 = top_nodes <= 1 ? 0.0 : static_cast<double>(
|
|
std::bit_width(top_nodes) - 1U);
|
|
const auto lookup = config_.tiered_read_base
|
|
+ config_.tiered_read_per_level * levels
|
|
+ config_.tiered_lookup_per_log_top * top_log2;
|
|
switch (sample.kind) {
|
|
case OperationKind::random_read:
|
|
return lookup * count;
|
|
case OperationKind::sequential_read:
|
|
return (config_.tiered_scan_element
|
|
+ config_.tiered_scan_boundary
|
|
/ static_cast<double>(tiered.leaf_capacity)) * count;
|
|
case OperationKind::append:
|
|
return (config_.tiered_append
|
|
+ 8.0 / static_cast<double>(tiered.leaf_capacity)) * count;
|
|
case OperationKind::set:
|
|
return lookup * 1.3 * count;
|
|
case OperationKind::insert:
|
|
case OperationKind::erase: {
|
|
const auto width_scale = std::max(1.0,
|
|
static_cast<double>(sample.element_bytes) / sizeof(std::uint32_t));
|
|
const auto local = sample.position % tiered.leaf_capacity;
|
|
const auto local_moves = std::min(local, tiered.leaf_capacity - local);
|
|
const auto directory_multiplier = localized
|
|
? config_.localized_directory_multiplier : 1.0;
|
|
const auto directory_work = config_.tiered_directory_unit
|
|
* directory_multiplier
|
|
* static_cast<double>(leaf_count);
|
|
return (config_.tiered_edit_fixed
|
|
+ config_.tiered_move_unit * width_scale
|
|
* static_cast<double>(local_moves)
|
|
+ directory_work + lookup) * count;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
struct DecisionEvaluation {
|
|
std::optional<AdaptationDecision> decision;
|
|
TieredConfig best_config{};
|
|
double vector_cost = 0.0;
|
|
double best_tiered_cost = 0.0;
|
|
double current_cost = 0.0;
|
|
double selected_rebuild_cost = 0.0;
|
|
std::size_t forecast_operations = 0;
|
|
};
|
|
|
|
[[nodiscard]] DecisionEvaluation evaluate_decision(
|
|
StorageMode current,
|
|
std::size_t size,
|
|
TieredConfig current_config) const noexcept {
|
|
DecisionEvaluation evaluation;
|
|
|
|
std::size_t best_index = 0;
|
|
for (std::size_t i = 1; i < candidate_count; ++i) {
|
|
if (ewma_candidate_costs_[i] < ewma_candidate_costs_[best_index]) {
|
|
best_index = i;
|
|
}
|
|
}
|
|
evaluation.best_config = candidate_config(best_index, size);
|
|
evaluation.vector_cost = ewma_vector_cost_;
|
|
evaluation.best_tiered_cost = ewma_candidate_costs_[best_index];
|
|
const auto current_tiered_cost = ewma_active_tiered_cost_;
|
|
evaluation.current_cost = current == StorageMode::vector
|
|
? evaluation.vector_cost : current_tiered_cost;
|
|
|
|
const auto width_scale = std::max(1.0,
|
|
static_cast<double>(last_element_bytes_) / sizeof(std::uint32_t));
|
|
const auto conversion_cost = static_cast<double>(size)
|
|
* config_.conversion_unit_per_element * width_scale;
|
|
evaluation.selected_rebuild_cost = conversion_cost;
|
|
const auto evidence_forecast = static_cast<std::size_t>(std::ceil(
|
|
total_weight_ * config_.forecast_growth_factor));
|
|
evaluation.forecast_operations = std::clamp(
|
|
evidence_forecast,
|
|
config_.minimum_forecast_operations,
|
|
config_.forecast_operations);
|
|
|
|
const bool enough_evidence =
|
|
total_weight_ >= static_cast<double>(config_.minimum_observations)
|
|
|| total_edit_weight_
|
|
>= static_cast<double>(config_.minimum_edit_observations);
|
|
if (!initialized_ || !enough_evidence
|
|
|| operations_since_transition_ < config_.minimum_residency_operations) {
|
|
return evaluation;
|
|
}
|
|
|
|
const auto forecast = static_cast<double>(evaluation.forecast_operations);
|
|
double best_net_saving = 0.0;
|
|
|
|
if (current == StorageMode::vector) {
|
|
if (size >= config_.minimum_tiered_size
|
|
&& ewma_edit_fraction_ >= config_.tiered_entry_edit_fraction
|
|
&& evaluation.best_tiered_cost < evaluation.vector_cost) {
|
|
const auto gross = (evaluation.vector_cost
|
|
- evaluation.best_tiered_cost) * forecast;
|
|
const auto threshold = conversion_cost
|
|
* config_.vector_to_tiered_safety;
|
|
if (gross > threshold) {
|
|
evaluation.decision = AdaptationDecision{
|
|
StorageMode::tiered, evaluation.best_config, gross};
|
|
best_net_saving = gross - threshold;
|
|
}
|
|
}
|
|
return evaluation;
|
|
}
|
|
|
|
// A tiered sequence returns to vector only after the workload has
|
|
// crossed the low (read-dominant) side of the phase hysteresis band.
|
|
if (ewma_edit_fraction_ <= config_.tiered_exit_edit_fraction
|
|
&& evaluation.vector_cost < current_tiered_cost) {
|
|
const auto gross = (current_tiered_cost - evaluation.vector_cost) * forecast;
|
|
const auto threshold = conversion_cost * config_.tiered_to_vector_safety;
|
|
if (gross > threshold) {
|
|
evaluation.decision = AdaptationDecision{
|
|
StorageMode::vector, normalize_tiered(current_config), gross};
|
|
best_net_saving = gross - threshold;
|
|
}
|
|
}
|
|
|
|
const auto normalized_current = normalize_tiered(current_config);
|
|
const bool shape_differs = evaluation.best_config != normalized_current;
|
|
const bool directory_only = evaluation.best_config.leaf_capacity
|
|
== normalized_current.leaf_capacity;
|
|
const auto shape_rebuild_cost = directory_only
|
|
? static_cast<double>((size + evaluation.best_config.leaf_capacity - 1)
|
|
/ evaluation.best_config.leaf_capacity)
|
|
* config_.directory_rebuild_unit_per_leaf
|
|
: conversion_cost;
|
|
const auto relative_improvement = current_tiered_cost > 0.0
|
|
? (current_tiered_cost - evaluation.best_tiered_cost)
|
|
/ current_tiered_cost
|
|
: 0.0;
|
|
if (operations_since_shape_transition_
|
|
>= config_.minimum_shape_residency_operations
|
|
&& ewma_edit_fraction_ >= config_.minimum_shape_edit_fraction
|
|
&& shape_differs
|
|
&& evaluation.best_tiered_cost < current_tiered_cost
|
|
&& relative_improvement >= config_.minimum_shape_improvement) {
|
|
const auto gross = (current_tiered_cost
|
|
- evaluation.best_tiered_cost) * forecast;
|
|
const auto threshold = shape_rebuild_cost * config_.tiered_rebuild_safety;
|
|
const auto net = gross - threshold;
|
|
if (gross > threshold && net > best_net_saving) {
|
|
evaluation.decision = AdaptationDecision{
|
|
StorageMode::tiered, evaluation.best_config, gross};
|
|
evaluation.selected_rebuild_cost = shape_rebuild_cost;
|
|
}
|
|
}
|
|
return evaluation;
|
|
}
|
|
|
|
void close_window() noexcept {
|
|
const auto inverse_weight = 1.0 / window_weight_;
|
|
const auto vector_per_operation = window_vector_cost_ * inverse_weight;
|
|
const auto active_per_operation = window_active_tiered_cost_ * inverse_weight;
|
|
const auto edit_fraction = window_edit_weight_ * inverse_weight;
|
|
std::array<double, candidate_count> candidates{};
|
|
for (std::size_t i = 0; i < candidate_count; ++i) {
|
|
candidates[i] = window_candidate_costs_[i] * inverse_weight;
|
|
}
|
|
if (!initialized_) {
|
|
ewma_vector_cost_ = vector_per_operation;
|
|
ewma_active_tiered_cost_ = active_per_operation;
|
|
ewma_candidate_costs_ = candidates;
|
|
ewma_edit_fraction_ = edit_fraction;
|
|
initialized_ = true;
|
|
} else {
|
|
const auto alpha = config_.ewma_alpha;
|
|
ewma_vector_cost_ = alpha * vector_per_operation
|
|
+ (1.0 - alpha) * ewma_vector_cost_;
|
|
ewma_active_tiered_cost_ = alpha * active_per_operation
|
|
+ (1.0 - alpha) * ewma_active_tiered_cost_;
|
|
for (std::size_t i = 0; i < candidate_count; ++i) {
|
|
ewma_candidate_costs_[i] = alpha * candidates[i]
|
|
+ (1.0 - alpha) * ewma_candidate_costs_[i];
|
|
}
|
|
ewma_edit_fraction_ = alpha * edit_fraction
|
|
+ (1.0 - alpha) * ewma_edit_fraction_;
|
|
}
|
|
if (window_edit_weight_ > 0.0) {
|
|
telemetry_.last_local_edit_fraction =
|
|
window_local_edit_weight_ / window_edit_weight_;
|
|
}
|
|
telemetry_.last_edit_fraction = ewma_edit_fraction_;
|
|
total_weight_ += window_weight_;
|
|
window_vector_cost_ = 0.0;
|
|
window_active_tiered_cost_ = 0.0;
|
|
window_candidate_costs_.fill(0.0);
|
|
window_weight_ = 0.0;
|
|
window_edit_weight_ = 0.0;
|
|
window_local_edit_weight_ = 0.0;
|
|
update_confirmation(evaluate_decision(
|
|
active_mode_, last_observed_size_, active_tiered_).decision);
|
|
decision_ready_ = true;
|
|
if (closed_windows_since_decision_
|
|
!= std::numeric_limits<std::size_t>::max()) {
|
|
++closed_windows_since_decision_;
|
|
}
|
|
}
|
|
|
|
static bool same_decision(const AdaptationDecision& left,
|
|
const AdaptationDecision& right) noexcept {
|
|
return left.target == right.target
|
|
&& left.tiered_config == right.tiered_config;
|
|
}
|
|
|
|
void update_confirmation(
|
|
const std::optional<AdaptationDecision>& decision) noexcept {
|
|
if (!decision) {
|
|
pending_decision_.reset();
|
|
confirmation_count_ = 0;
|
|
return;
|
|
}
|
|
if (pending_decision_ && same_decision(*pending_decision_, *decision)) {
|
|
confirmation_count_ = std::min(
|
|
config_.required_confirmation_windows,
|
|
confirmation_count_ + 1);
|
|
pending_decision_ = decision;
|
|
} else {
|
|
pending_decision_ = decision;
|
|
confirmation_count_ = 1;
|
|
}
|
|
}
|
|
|
|
[[nodiscard]] std::optional<AdaptationDecision>
|
|
confirm(const std::optional<AdaptationDecision>& decision) noexcept {
|
|
if (!decision || !pending_decision_
|
|
|| !same_decision(*pending_decision_, *decision)
|
|
|| confirmation_count_ < config_.required_confirmation_windows) {
|
|
return std::nullopt;
|
|
}
|
|
auto confirmed = decision;
|
|
pending_decision_.reset();
|
|
confirmation_count_ = 0;
|
|
return confirmed;
|
|
}
|
|
|
|
void synchronize_active_tiered_ewma(TieredConfig active) noexcept {
|
|
if (!initialized_) {
|
|
return;
|
|
}
|
|
for (std::size_t i = 0; i < candidate_count; ++i) {
|
|
if (config_.tiered_leaf_candidates[i] == active.leaf_capacity) {
|
|
ewma_active_tiered_cost_ = ewma_candidate_costs_[i];
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// A committed transition starts a new payback/residency epoch, but the
|
|
// workload signal is deliberately retained. Throwing the EWMA away made
|
|
// the policy relearn the same stationary phase from noisy short windows and
|
|
// allowed vector<->tiered and leaf-shape ping-pong.
|
|
void clear_transition_evidence() noexcept {
|
|
operations_since_transition_ = 0;
|
|
operations_since_shape_transition_ = 0;
|
|
total_weight_ = 0.0;
|
|
total_edit_weight_ = 0.0;
|
|
window_weight_ = 0.0;
|
|
window_vector_cost_ = 0.0;
|
|
window_active_tiered_cost_ = 0.0;
|
|
window_candidate_costs_.fill(0.0);
|
|
window_edit_weight_ = 0.0;
|
|
window_local_edit_weight_ = 0.0;
|
|
pending_decision_.reset();
|
|
confirmation_count_ = 0;
|
|
has_last_edit_ = false;
|
|
decision_ready_ = false;
|
|
closed_windows_since_decision_ = 0;
|
|
}
|
|
|
|
AdaptationConfig config_;
|
|
TieredConfig active_tiered_;
|
|
StorageMode active_mode_ = StorageMode::vector;
|
|
std::size_t search_fanout_ = 64;
|
|
std::size_t search_max_levels_ = 4;
|
|
AdaptationTelemetry telemetry_;
|
|
std::array<double, candidate_count> window_candidate_costs_{};
|
|
std::array<double, candidate_count> ewma_candidate_costs_{};
|
|
double window_vector_cost_ = 0.0;
|
|
double window_active_tiered_cost_ = 0.0;
|
|
double window_weight_ = 0.0;
|
|
double window_edit_weight_ = 0.0;
|
|
double window_local_edit_weight_ = 0.0;
|
|
double total_weight_ = 0.0;
|
|
double total_edit_weight_ = 0.0;
|
|
double ewma_vector_cost_ = 0.0;
|
|
double ewma_active_tiered_cost_ = 0.0;
|
|
double ewma_edit_fraction_ = 0.0;
|
|
std::size_t operations_since_transition_ = 0;
|
|
std::size_t operations_since_shape_transition_ = 0;
|
|
std::size_t last_edit_position_ = 0;
|
|
std::size_t last_observed_size_ = 0;
|
|
std::size_t last_element_bytes_ = sizeof(std::uint32_t);
|
|
std::size_t confirmation_count_ = 0;
|
|
std::optional<AdaptationDecision> pending_decision_;
|
|
bool has_last_edit_ = false;
|
|
bool initialized_ = false;
|
|
bool decision_ready_ = false;
|
|
std::size_t closed_windows_since_decision_ = 0;
|
|
};
|
|
|
|
} // namespace uc
|