feat(governor): add adaptive batch sizing

This commit is contained in:
fy59 2026-08-07 13:11:40 +02:00
parent d1292bf58e
commit 4a0b1df412
3 changed files with 365 additions and 3 deletions

View file

@ -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

View file

@ -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,

View file

@ -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;
}