v7-7: Implement Learner for dynamic C5 route switching
- Add SmallLearnerStatsV7 type + API to policy box - Hook ColdIface refill/retire to collect stats (capacity-based) - Implement C5 route switching: if C5 ratio < 30%, switch to MID_V3 - Version-based TLS cache invalidation for policy updates - Evaluation interval: every 100 refills Tested with c6heavy scenario: C5 ratio=12% triggers V7 → MID_V3 switch 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@ -1,9 +1,10 @@
|
||||
// smallobject_policy_v7_box.h - SmallObject Policy v7 (Phase v7-4)
|
||||
// smallobject_policy_v7_box.h - SmallObject Policy v7 (Phase v7-7: Learner integration)
|
||||
//
|
||||
// Purpose:
|
||||
// - Centralized routing policy for ULTRA / v7 / MID_v3 / LEGACY
|
||||
// - Single source of truth for class → route_kind mapping
|
||||
// - ENV configuration managed in one place (L3 Policy layer)
|
||||
// - Learner: dynamic route switching based on workload stats (v7-7)
|
||||
|
||||
#ifndef HAKMEM_SMALLOBJECT_POLICY_V7_BOX_H
|
||||
#define HAKMEM_SMALLOBJECT_POLICY_V7_BOX_H
|
||||
@ -46,4 +47,58 @@ void small_policy_v7_init_from_env(SmallPolicyV7* policy);
|
||||
/// Get route kind name for debugging
|
||||
const char* small_route_kind_name(SmallRouteKind kind);
|
||||
|
||||
// ============================================================================
|
||||
// Learner Stats (Phase v7-7)
|
||||
// ============================================================================
|
||||
|
||||
/// Per-class stats for Learner
|
||||
typedef struct SmallLearnerClassStatsV7 {
|
||||
uint64_t total_allocs; // Total allocs for this class (all routes)
|
||||
uint64_t v7_allocs; // Allocs via v7 route
|
||||
uint64_t v7_retires; // Page retires from v7
|
||||
uint32_t sample_count; // Number of samples
|
||||
} SmallLearnerClassStatsV7;
|
||||
|
||||
/// Aggregate Learner stats
|
||||
typedef struct SmallLearnerStatsV7 {
|
||||
SmallLearnerClassStatsV7 per_class[8]; // C0-C7
|
||||
uint64_t total_retires; // Total page retires
|
||||
uint64_t eval_count; // Number of evaluations
|
||||
} SmallLearnerStatsV7;
|
||||
|
||||
// ============================================================================
|
||||
// Learner API (Phase v7-7)
|
||||
// ============================================================================
|
||||
|
||||
/// Record a page retire for Learner stats (called from ColdIface)
|
||||
/// @param class_idx: Size class of the retired page
|
||||
/// @param capacity: Capacity of the page (used as traffic proxy)
|
||||
void small_learner_v7_record_retire(uint32_t class_idx, uint64_t capacity);
|
||||
|
||||
/// Record a page refill for Learner stats (called from ColdIface)
|
||||
/// @param class_idx: Size class of the refilled page
|
||||
/// @param capacity: Capacity of the page (used as traffic proxy)
|
||||
void small_learner_v7_record_refill(uint32_t class_idx, uint64_t capacity);
|
||||
|
||||
/// Update policy from Learner stats (called periodically)
|
||||
/// Decision rule: if C5 alloc ratio > 30%, route C5 to v7, else MID_v3
|
||||
/// @param stats: Learner stats to evaluate
|
||||
/// @param policy_out: Policy to update
|
||||
void small_policy_v7_update_from_learner(
|
||||
const SmallLearnerStatsV7* stats,
|
||||
SmallPolicyV7* policy_out
|
||||
);
|
||||
|
||||
/// Get current Learner stats snapshot (for debugging/OBSERVE)
|
||||
const SmallLearnerStatsV7* small_learner_v7_stats_snapshot(void);
|
||||
|
||||
/// Force re-evaluation of policy from current stats
|
||||
void small_learner_v7_evaluate(void);
|
||||
|
||||
/// Learner threshold for C5 route decision (default: 30%)
|
||||
#define SMALL_LEARNER_C5_THRESHOLD_PCT 30
|
||||
|
||||
/// Evaluation interval (every N refills)
|
||||
#define SMALL_LEARNER_EVAL_INTERVAL 100
|
||||
|
||||
#endif // HAKMEM_SMALLOBJECT_POLICY_V7_BOX_H
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
#include "box/smallobject_cold_iface_v7_box.h"
|
||||
#include "box/smallsegment_v7_box.h"
|
||||
#include "box/region_id_v6_box.h"
|
||||
#include "box/smallobject_policy_v7_box.h" // v7-7: Learner integration
|
||||
#include "tiny_region_id.h" // v7-5a: For HEADER_MAGIC, HEADER_CLASS_MASK
|
||||
|
||||
#ifndef likely
|
||||
@ -183,6 +184,9 @@ SmallPageMeta_v7* small_cold_v7_refill_page(SmallHeapCtx_v7* ctx, uint32_t class
|
||||
SmallClassHeap_v7* heap = &ctx->cls[class_idx];
|
||||
heap->current = page;
|
||||
|
||||
// v7-7: Feed refill to Learner for workload detection
|
||||
small_learner_v7_record_refill(class_idx, capacity);
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
@ -231,11 +235,13 @@ void small_cold_v7_retire_page(SmallHeapCtx_v7* ctx, SmallPageMeta_v7* page) {
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Stats Publishing (stub for now)
|
||||
// Stats Publishing (v7-7: Learner integration)
|
||||
// ============================================================================
|
||||
|
||||
void small_cold_v7_publish_stats(const SmallPageStatsV7* stats) {
|
||||
// TODO: Future integration with Learner/PolicyBox
|
||||
// For now, just a no-op
|
||||
(void)stats;
|
||||
if (!stats) return;
|
||||
|
||||
// v7-7: Feed stats to Learner for dynamic route switching
|
||||
// Note: v7-5a removed alloc_count from hot path, use capacity as traffic proxy
|
||||
small_learner_v7_record_retire(stats->class_idx, stats->capacity);
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// smallobject_policy_v7.c - Policy Box implementation
|
||||
// smallobject_policy_v7.c - Policy Box implementation (Phase v7-7: Learner integration)
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@ -11,16 +11,41 @@
|
||||
#endif
|
||||
|
||||
// ============================================================================
|
||||
// TLS Policy Snapshot
|
||||
// Learner Stats (Global, v7-7)
|
||||
// ============================================================================
|
||||
|
||||
static SmallLearnerStatsV7 g_small_learner_stats_v7;
|
||||
static int g_learner_v7_enabled = -1; // -1: uninit, 0: disabled, 1: enabled
|
||||
|
||||
static inline int learner_v7_enabled(void) {
|
||||
if (unlikely(g_learner_v7_enabled < 0)) {
|
||||
// Enable Learner only when v7 is enabled
|
||||
const char* e = getenv("HAKMEM_SMALL_HEAP_V7_ENABLED");
|
||||
g_learner_v7_enabled = (e && *e && *e != '0') ? 1 : 0;
|
||||
}
|
||||
return g_learner_v7_enabled;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TLS Policy Snapshot (v7-7: version-based invalidation)
|
||||
// ============================================================================
|
||||
|
||||
static uint32_t g_policy_v7_version = 0; // Global version, incremented by Learner
|
||||
|
||||
static __thread SmallPolicyV7 g_small_policy_v7;
|
||||
static __thread int g_small_policy_v7_init = 0;
|
||||
static __thread uint32_t g_small_policy_v7_version = 0; // TLS cached version
|
||||
|
||||
const SmallPolicyV7* small_policy_v7_snapshot(void) {
|
||||
if (unlikely(!g_small_policy_v7_init)) {
|
||||
// Check if TLS cache is stale (version mismatch or uninitialized)
|
||||
if (unlikely(g_small_policy_v7_version != g_policy_v7_version || g_policy_v7_version == 0)) {
|
||||
small_policy_v7_init_from_env(&g_small_policy_v7);
|
||||
g_small_policy_v7_init = 1;
|
||||
|
||||
// v7-7: Apply Learner-driven route updates
|
||||
if (learner_v7_enabled() && g_small_learner_stats_v7.total_retires > 0) {
|
||||
small_policy_v7_update_from_learner(&g_small_learner_stats_v7, &g_small_policy_v7);
|
||||
}
|
||||
|
||||
g_small_policy_v7_version = g_policy_v7_version ? g_policy_v7_version : 1;
|
||||
}
|
||||
return &g_small_policy_v7;
|
||||
}
|
||||
@ -116,3 +141,101 @@ const char* small_route_kind_name(SmallRouteKind kind) {
|
||||
default: return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Learner Implementation (Phase v7-7)
|
||||
// ============================================================================
|
||||
|
||||
// Total refills (for evaluation interval)
|
||||
static uint64_t g_small_learner_total_refills = 0;
|
||||
|
||||
void small_learner_v7_record_refill(uint32_t class_idx, uint64_t capacity) {
|
||||
if (!learner_v7_enabled()) return;
|
||||
if (class_idx >= 8) return;
|
||||
|
||||
// Record stats: refill indicates page was needed (current page exhausted)
|
||||
SmallLearnerClassStatsV7* cls = &g_small_learner_stats_v7.per_class[class_idx];
|
||||
cls->v7_allocs += capacity; // Use capacity as proxy for traffic volume
|
||||
cls->sample_count++;
|
||||
|
||||
g_small_learner_total_refills++;
|
||||
|
||||
// Periodic evaluation (on refills)
|
||||
if (g_small_learner_total_refills % SMALL_LEARNER_EVAL_INTERVAL == 0) {
|
||||
small_learner_v7_evaluate();
|
||||
}
|
||||
}
|
||||
|
||||
void small_learner_v7_record_retire(uint32_t class_idx, uint64_t capacity) {
|
||||
if (!learner_v7_enabled()) return;
|
||||
if (class_idx >= 8) return;
|
||||
|
||||
// Record stats (atomic would be better for multi-thread, but keep simple for now)
|
||||
// Note: v7-5a removed per-page alloc_count from hot path, so we use capacity instead
|
||||
// capacity represents "slots available on page" which approximates traffic volume
|
||||
SmallLearnerClassStatsV7* cls = &g_small_learner_stats_v7.per_class[class_idx];
|
||||
cls->v7_retires++;
|
||||
|
||||
g_small_learner_stats_v7.total_retires++;
|
||||
|
||||
(void)capacity; // Not used for now (kept for API compatibility)
|
||||
}
|
||||
|
||||
void small_policy_v7_update_from_learner(
|
||||
const SmallLearnerStatsV7* stats,
|
||||
SmallPolicyV7* policy_out
|
||||
) {
|
||||
if (!stats || !policy_out) return;
|
||||
|
||||
// Calculate total allocs across all classes
|
||||
uint64_t total_allocs = 0;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
total_allocs += stats->per_class[i].v7_allocs;
|
||||
}
|
||||
|
||||
if (total_allocs == 0) return; // No data yet
|
||||
|
||||
// C5 decision: if C5 ratio > threshold, route to v7, else MID_v3
|
||||
uint64_t c5_allocs = stats->per_class[5].v7_allocs;
|
||||
uint64_t c5_ratio_pct = (c5_allocs * 100) / total_allocs;
|
||||
|
||||
SmallRouteKind old_c5_route = policy_out->route_kind[5];
|
||||
SmallRouteKind new_c5_route;
|
||||
|
||||
if (c5_ratio_pct >= SMALL_LEARNER_C5_THRESHOLD_PCT) {
|
||||
// C5-heavy workload → keep C5 on v7
|
||||
new_c5_route = SMALL_ROUTE_V7;
|
||||
} else {
|
||||
// Mixed workload → move C5 to MID_v3
|
||||
new_c5_route = SMALL_ROUTE_MID_V3;
|
||||
}
|
||||
|
||||
// Only log and update if route changed
|
||||
if (old_c5_route != new_c5_route) {
|
||||
// Log only the first switch (to avoid spam)
|
||||
static int g_learner_v7_switch_logged = 0;
|
||||
if (!g_learner_v7_switch_logged) {
|
||||
g_learner_v7_switch_logged = 1;
|
||||
fprintf(stderr, "[LEARNER_V7] C5 route switch: %s → %s (C5 ratio=%lu%%, threshold=%d%%)\n",
|
||||
small_route_kind_name(old_c5_route),
|
||||
small_route_kind_name(new_c5_route),
|
||||
(unsigned long)c5_ratio_pct,
|
||||
SMALL_LEARNER_C5_THRESHOLD_PCT);
|
||||
}
|
||||
policy_out->route_kind[5] = new_c5_route;
|
||||
}
|
||||
}
|
||||
|
||||
const SmallLearnerStatsV7* small_learner_v7_stats_snapshot(void) {
|
||||
return &g_small_learner_stats_v7;
|
||||
}
|
||||
|
||||
void small_learner_v7_evaluate(void) {
|
||||
if (!learner_v7_enabled()) return;
|
||||
|
||||
// Increment global version to invalidate all TLS caches
|
||||
// Next call to small_policy_v7_snapshot() will re-apply Learner updates
|
||||
__sync_fetch_and_add(&g_policy_v7_version, 1);
|
||||
|
||||
g_small_learner_stats_v7.eval_count++;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user