From 4a0b1df41255eb995305a9c377d54d5e2e931952 Mon Sep 17 00:00:00 2001 From: fy59 Date: Fri, 7 Aug 2026 13:11:40 +0200 Subject: [PATCH] feat(governor): add adaptive batch sizing --- include/lardon3d/resource_governor.h | 10 ++ src/resource_governor.c | 150 ++++++++++++++++++- tests/test_resource_governor.c | 208 ++++++++++++++++++++++++++- 3 files changed, 365 insertions(+), 3 deletions(-) diff --git a/include/lardon3d/resource_governor.h b/include/lardon3d/resource_governor.h index 69f69e2..710a02c 100644 --- a/include/lardon3d/resource_governor.h +++ b/include/lardon3d/resource_governor.h @@ -187,5 +187,15 @@ bool lardon3d_resource_governor_wait_for_change( const char *lardon3d_resource_decision_name( Lardon3DResourceDecisionKind kind ); +/* Enregistre les métriques d'un lot terminé pour l'adaptation dynamique + * de la taille des lots futurs. Le buffer est borné (8 entrées par classe + * de tâche). Thread-safe. */ +bool lardon3d_resource_governor_record_batch( + Lardon3DResourceGovernor *governor, + Lardon3DResourceTaskClass task_class, + size_t batch_size, + uint64_t duration_ns, + size_t peak_memory_bytes +); #endif diff --git a/src/resource_governor.c b/src/resource_governor.c index ac61f03..f65f6af 100644 --- a/src/resource_governor.c +++ b/src/resource_governor.c @@ -12,6 +12,16 @@ struct Lardon3DResourceReservation { struct Lardon3DResourceReservation *next; }; +enum { + LARDON3D_BATCH_METRICS_CAPACITY = 8, +}; + +typedef struct { + size_t batch_size; + uint64_t duration_ns; + size_t peak_memory_bytes; +} Lardon3DBatchMetrics; + struct Lardon3DResourceGovernor { pthread_mutex_t mutex; pthread_cond_t cond; @@ -24,6 +34,9 @@ struct Lardon3DResourceGovernor { unsigned int cpu_reserved; unsigned int gpu_slots_reserved; unsigned int io_slots_reserved; + Lardon3DBatchMetrics batch_metrics[LARDON3D_RESOURCE_TASK_MIXED + 1][LARDON3D_BATCH_METRICS_CAPACITY]; + size_t batch_metrics_count[LARDON3D_RESOURCE_TASK_MIXED + 1]; + size_t batch_metrics_head[LARDON3D_RESOURCE_TASK_MIXED + 1]; size_t active_count; Lardon3DResourceReservation *active; Lardon3DResourceReservation *released; @@ -145,6 +158,94 @@ batch_capacity(uint64_t available, uint64_t fixed, uint64_t per_item) return capacity > SIZE_MAX ? SIZE_MAX : (size_t)capacity; } +static bool +record_batch_locked( + Lardon3DResourceGovernor *governor, + Lardon3DResourceTaskClass task_class, + size_t batch_size, + uint64_t duration_ns, + size_t peak_memory_bytes +) +{ + if (batch_size == 0) { + return false; + } + size_t class_index = (size_t)task_class; + if (class_index > (size_t)LARDON3D_RESOURCE_TASK_MIXED) { + return false; + } + Lardon3DBatchMetrics *metrics = governor->batch_metrics[class_index]; + size_t count = governor->batch_metrics_count[class_index]; + size_t head = governor->batch_metrics_head[class_index]; + metrics[head] = (Lardon3DBatchMetrics) { + .batch_size = batch_size, + .duration_ns = duration_ns, + .peak_memory_bytes = peak_memory_bytes, + }; + head = (head + 1) % LARDON3D_BATCH_METRICS_CAPACITY; + governor->batch_metrics_head[class_index] = head; + if (count < LARDON3D_BATCH_METRICS_CAPACITY) { + governor->batch_metrics_count[class_index] = count + 1; + } + return true; +} + +static size_t +adaptive_batch_limit( + const Lardon3DResourceGovernor *governor, + Lardon3DResourceTaskClass task_class, + size_t static_batch, + uint64_t memory_bytes_per_item +) +{ + if (memory_bytes_per_item == 0 || static_batch == 0) { + return static_batch; + } + size_t class_index = (size_t)task_class; + if (class_index > (size_t)LARDON3D_RESOURCE_TASK_MIXED) { + return static_batch; + } + size_t count = governor->batch_metrics_count[class_index]; + if (count == 0) { + return static_batch; + } + size_t head = governor->batch_metrics_head[class_index]; + uint64_t measured_per_item = 0; + size_t samples = 0; + for (size_t i = 0; i < count; ++i) { + size_t idx = (head + LARDON3D_BATCH_METRICS_CAPACITY - count + i) + % LARDON3D_BATCH_METRICS_CAPACITY; + const Lardon3DBatchMetrics *m = &governor->batch_metrics[class_index][idx]; + if (m->batch_size > 0 && m->peak_memory_bytes > 0) { + /* Coût par élément le plus défavorable observé : une moyenne + * sous-estimerait le pic et laisserait un lot dépasser son + * budget. La stabilité de l'hôte prime sur le débit. */ + uint64_t per_item = m->peak_memory_bytes / m->batch_size + + (m->peak_memory_bytes % m->batch_size != 0); + if (per_item > measured_per_item) { + measured_per_item = per_item; + } + ++samples; + } + } + if (samples == 0) { + return static_batch; + } + if (measured_per_item <= memory_bytes_per_item) { + return static_batch; + } + /* Éviter l'overflow de la multiplication : si static_batch est trop + * grand pour être multiplié sans débordement, on retourne 1 (le lot le + * plus conservateur possible) plutôt que de saturer à SIZE_MAX. */ + if (static_batch > UINT64_MAX / memory_bytes_per_item) { + return 1; + } + uint64_t corrected = (uint64_t)static_batch * memory_bytes_per_item + / measured_per_item; + size_t result = corrected > SIZE_MAX ? SIZE_MAX : (size_t)corrected; + return result > 0 ? result : 1; +} + static void set_decision( Lardon3DResourceDecision *decision, @@ -524,7 +625,20 @@ evaluate_locked( ) ); } - batch = minimum_size(batch, estimate->maximum_batch_size); + /* Le lot maximal visé est corrigé par les métriques mesurées : c'est la + * nouvelle cible du contrat, pas une réduction faute de ressources. La + * correction ne descend jamais sous minimum_batch_size pour éviter un + * WAIT persistant. */ + size_t adapted_maximum = adaptive_batch_limit( + governor, + estimate->task_class, + estimate->maximum_batch_size, + memory_per_item + ); + if (adapted_maximum < estimate->minimum_batch_size) { + adapted_maximum = estimate->minimum_batch_size; + } + batch = minimum_size(batch, adapted_maximum); if (batch < estimate->minimum_batch_size) { set_decision(decision, LARDON3D_RESOURCE_WAIT, 0, 0, 0, 0, "Ressources déjà réservées ou temporairement insuffisantes."); return; @@ -547,7 +661,7 @@ evaluate_locked( unsigned int io = estimate->desired_io_slots < available.io_slots_available ? estimate->desired_io_slots : available.io_slots_available; - bool reduced = batch < estimate->maximum_batch_size + bool reduced = batch < adapted_maximum || cpu < estimate->desired_cpu_threads || gpu < estimate->desired_gpu_slots || io < estimate->desired_io_slots; @@ -820,6 +934,38 @@ lardon3d_resource_governor_reservation_count( return count; } +bool +lardon3d_resource_governor_record_batch( + Lardon3DResourceGovernor *governor, + Lardon3DResourceTaskClass task_class, + size_t batch_size, + uint64_t duration_ns, + size_t peak_memory_bytes +) +{ + if (!governor) { + return false; + } + if (batch_size == 0) { + /* No-op réussi : aucune métrique, aucun réveil inutile. */ + return true; + } + (void)pthread_mutex_lock(&governor->mutex); + bool recorded = record_batch_locked( + governor, + task_class, + batch_size, + duration_ns, + peak_memory_bytes + ); + if (recorded) { + ++governor->generation; + (void)pthread_cond_broadcast(&governor->cond); + } + (void)pthread_mutex_unlock(&governor->mutex); + return recorded; +} + bool lardon3d_resource_governor_decide( Lardon3DResourceGovernor *governor, diff --git a/tests/test_resource_governor.c b/tests/test_resource_governor.c index 907f49b..4baa9be 100644 --- a/tests/test_resource_governor.c +++ b/tests/test_resource_governor.c @@ -391,10 +391,216 @@ run_generation_test(void) return true; } +static bool +run_adaptive_batch_test(void) +{ + Lardon3DHardwareProfile profile = { + .logical_cpu_count = 16, + .page_size_bytes = 4096, + .memory_total_bytes = GIBIBYTES(16), + .cpu_architecture = "test", + }; + Lardon3DResourcePolicy policy = { + .system_memory_reserve_bytes = GIBIBYTES(2), + .gpu_memory_reserve_bytes = 0, + .system_cpu_reserve = 1, + .maximum_cpu_load_ratio = 0.90, + .maximum_io_pressure_avg10 = 80.0, + .io_slot_capacity = 8, + }; + Lardon3DResourceGovernor *governor = lardon3d_resource_governor_create( + &profile, + &policy + ); + CHECK(governor); + + Lardon3DResourceSnapshot snapshot = { + .memory_available_bytes = GIBIBYTES(10), + .cpu_load_1m = 2.0, + }; + Lardon3DResourceDecision decision; + + /* Test 1: Without metrics, batch is unchanged */ + CHECK(lardon3d_resource_governor_decide(governor, &snapshot, &(Lardon3DResourceRequest) { + .memory_bytes_per_item = MEBIBYTES(100), + .minimum_batch_size = 2, + .preferred_batch_size = 8, + .requested_cpu_threads = 4, + }, &decision)); + CHECK(decision.kind == LARDON3D_RESOURCE_START); + CHECK(decision.batch_size == 8); + + /* Test 2: Record batch matching estimate → no reduction */ + CHECK(lardon3d_resource_governor_record_batch( + governor, + LARDON3D_RESOURCE_TASK_GENERAL, + 8, + 1000000000ULL, + MEBIBYTES(800) + )); + CHECK(lardon3d_resource_governor_decide(governor, &snapshot, &(Lardon3DResourceRequest) { + .memory_bytes_per_item = MEBIBYTES(100), + .minimum_batch_size = 2, + .preferred_batch_size = 8, + .requested_cpu_threads = 4, + }, &decision)); + CHECK(decision.kind == LARDON3D_RESOURCE_START); + CHECK(decision.batch_size == 8); + + /* Test 3: Record batch with higher memory usage → batch reduced */ + CHECK(lardon3d_resource_governor_record_batch( + governor, + LARDON3D_RESOURCE_TASK_GENERAL, + 8, + 1000000000ULL, + MEBIBYTES(1600) + )); + CHECK(lardon3d_resource_governor_decide(governor, &snapshot, &(Lardon3DResourceRequest) { + .memory_bytes_per_item = MEBIBYTES(100), + .minimum_batch_size = 2, + .preferred_batch_size = 8, + .requested_cpu_threads = 4, + }, &decision)); + CHECK(decision.kind == LARDON3D_RESOURCE_START); + CHECK(decision.batch_size == 4); + + /* Test 4: Buffer overflow (9 writes) → oldest overwritten */ + for (size_t i = 0; i < 9; ++i) { + CHECK(lardon3d_resource_governor_record_batch( + governor, + LARDON3D_RESOURCE_TASK_GENERAL, + 4, + 1000000000ULL, + MEBIBYTES(400) + )); + } + CHECK(lardon3d_resource_governor_decide(governor, &snapshot, &(Lardon3DResourceRequest) { + .memory_bytes_per_item = MEBIBYTES(100), + .minimum_batch_size = 2, + .preferred_batch_size = 8, + .requested_cpu_threads = 4, + }, &decision)); + CHECK(decision.kind == LARDON3D_RESOURCE_START); + CHECK(decision.batch_size == 8); + + /* Test 5: Different task class not affected */ + CHECK(lardon3d_resource_governor_record_batch( + governor, + LARDON3D_RESOURCE_TASK_IO, + 8, + 1000000000ULL, + MEBIBYTES(1600) + )); + CHECK(lardon3d_resource_governor_decide(governor, &snapshot, &(Lardon3DResourceRequest) { + .memory_bytes_per_item = MEBIBYTES(100), + .minimum_batch_size = 2, + .preferred_batch_size = 8, + .requested_cpu_threads = 4, + }, &decision)); + CHECK(decision.kind == LARDON3D_RESOURCE_START); + CHECK(decision.batch_size == 8); + + /* Test 6: record_batch increments generation */ + uint64_t gen_before = lardon3d_resource_governor_generation(governor); + CHECK(lardon3d_resource_governor_record_batch( + governor, + LARDON3D_RESOURCE_TASK_GENERAL, + 4, + 1000000000ULL, + MEBIBYTES(400) + )); + CHECK(lardon3d_resource_governor_generation(governor) == gen_before + 1); + + /* Test 7: NULL governor returns false */ + CHECK(!lardon3d_resource_governor_record_batch(NULL, LARDON3D_RESOURCE_TASK_GENERAL, 4, 0, 0)); + + /* Test 8: Zero batch_size is ignored */ + CHECK(lardon3d_resource_governor_record_batch(governor, LARDON3D_RESOURCE_TASK_GENERAL, 0, 0, 0)); + + /* T1: Invalid task class → false, generation unchanged */ + gen_before = lardon3d_resource_governor_generation(governor); + CHECK(!lardon3d_resource_governor_record_batch( + governor, + (Lardon3DResourceTaskClass)999, + 4, + 1000000000ULL, + MEBIBYTES(400) + )); + CHECK(lardon3d_resource_governor_generation(governor) == gen_before); + + /* T2: batch_size == 0 → true, generation unchanged */ + gen_before = lardon3d_resource_governor_generation(governor); + CHECK(lardon3d_resource_governor_record_batch( + governor, + LARDON3D_RESOURCE_TASK_GENERAL, + 0, + 0, + 0 + )); + CHECK(lardon3d_resource_governor_generation(governor) == gen_before); + + /* T3: minimum_batch_size guaranteed */ + for (size_t i = 0; i < 8; ++i) { + CHECK(lardon3d_resource_governor_record_batch( + governor, + LARDON3D_RESOURCE_TASK_GENERAL, + 8, + 1000000000ULL, + GIBIBYTES(8) + )); + } + CHECK(lardon3d_resource_governor_decide(governor, &snapshot, &(Lardon3DResourceRequest) { + .memory_bytes_per_item = MEBIBYTES(100), + .minimum_batch_size = 4, + .preferred_batch_size = 8, + .requested_cpu_threads = 4, + }, &decision)); + CHECK(decision.kind == LARDON3D_RESOURCE_START); + CHECK(decision.batch_size == 4); + + /* T4: Overflow of static_batch * memory_bytes_per_item in uint64_t. + * With memory_bytes_per_item = 2 and preferred_batch_size = SIZE_MAX, + * the multiplication SIZE_MAX * 2 would overflow uint64_t. The overflow + * guard in adaptive_batch_limit returns 1 (most conservative). The result + * is never SIZE_MAX, the adaptive limit never increases the batch, and + * the final result is clamped to the minimum expected value. */ + CHECK(lardon3d_resource_governor_record_batch( + governor, + LARDON3D_RESOURCE_TASK_GENERAL, + 1, + 1000000000ULL, + 100 + )); + CHECK(lardon3d_resource_governor_decide(governor, &snapshot, &(Lardon3DResourceRequest) { + .memory_bytes_per_item = 2, + .minimum_batch_size = 1, + .preferred_batch_size = SIZE_MAX, + .requested_cpu_threads = 1, + }, &decision)); + CHECK(decision.kind == LARDON3D_RESOURCE_START); + CHECK(decision.batch_size != SIZE_MAX); + CHECK(decision.batch_size >= 1); + CHECK(decision.batch_size <= 1); + + /* T5: generation increment only on real record */ + gen_before = lardon3d_resource_governor_generation(governor); + CHECK(lardon3d_resource_governor_record_batch( + governor, + LARDON3D_RESOURCE_TASK_IO, + 4, + 1000000000ULL, + MEBIBYTES(400) + )); + CHECK(lardon3d_resource_governor_generation(governor) == gen_before + 1); + + lardon3d_resource_governor_destroy(governor); + return true; +} + int main(void) { - return (run_test() && run_generation_test()) + return (run_test() && run_generation_test() && run_adaptive_batch_test()) ? EXIT_SUCCESS : EXIT_FAILURE; }