On 3/12/26 10:43, Christian Loehle wrote:
> On 3/11/26 14:26, Christian Loehle wrote:
>> On 3/11/26 13:44, Christian Loehle wrote:
>>> On 3/11/26 13:23, Juri Lelli wrote:
>>>> On 11/03/26 09:31, Christian Loehle wrote:
>>>>> On 3/6/26 16:10, Juri Lelli wrote:
>>>>
>>>> ...
>>>>
>>>>>> + /* Start one cpuhog per CPU at max bandwidth */
>>>>>> + printf(" Starting %d cpuhog tasks at max bandwidth...\n",
>>>>>> num_cpus);
>>>>>> +
>>>>>> + for (i = 0; i < num_cpus; i++) {
>>>>>> + pids[i] = dl_create_cpuhog(runtime_ns, deadline_ns,
>>>>>> period_ns, 0);
>>>>>> + if (pids[i] < 0) {
>>>>>> + printf(" Task %d failed to start: %s\n",
>>>>>> + i + 1, strerror(errno));
>>>>>> + goto cleanup;
>>>>>> + }
>>>>>> + started++;
>>>>>> + }
>>>>>
>>>>> Would it be okay to just have one task per max-cap CPU to make this pass
>>>>> on HMP?
>>>>> Or something more sophisticated?
>>>>>
>>>>
>>>> On HMP we should probably have max bandwidth hogs on big CPUs and then
>>>> scale runtime (bandwidth) considering smaller CPUs capacities. Cannot
>>>> quickly check atm, but that info (max cap per-CPU) is available
>>>> somewhere in sys or proc, is it?
>>>
>>> Yes it's here:
>>> /sys/devices/system/cpu/cpu0/cpu_capacity
>>>
>>> FWIW I've attached the two patches to get a pass out of arm64 HMP.
>>
>> Wait nevermind, this isn't right, this would expect a 10 CPU system with
>> [1024, 128, 128, 128, 128, 128, 128, 128, 128, 128]
>> = 2176
>> would allow for 2 1024-equivalent hogs, but that is obviously wrong as
>> the capacity -> bandwidth calculation must be capped in practice by
>> only summing the k-highest-cap-CPUs if there's only k deadline-tasks.
>>
>> Let me go and read how this is actually supposed to work.
>
> Nevermind the nevermind, it's a bit counterintuitive because we specifically
> test this edgecase here but my original proposal is fine...
>
> if you're still taking suggestions, I think a test with hotplugging and
> bandwidth would be nice, too:
>
> -Fill to max, verify extra admission fails.
> -Kill one task, offline one CPU, verify offline succeeds.
> -Try respawn while CPU is offline, verify admission fails.
> -Online CPU again, verify respawn succeeds.
>
For completeness, although I'm sure you can come up with something equally good:
# ===== START =====
# TEST: bandwidth_hotplug_accounting
# DESCRIPTION: Verify capacity-scaled bandwidth accounting across CPU hotplug
# OUTPUT:
# RT bandwidth: runtime=950000µs, period=1000000µs (95%)
# Number of online CPUs: 12
# Equivalent max-capacity CPUs: 8.47
# Max-capacity CPU: 0 (capacity=1024)
# DL server overhead on max-capacity CPU: 10.00%
# Task params: runtime=85ms, deadline=100ms, period=100ms
# Expected tasks at max-capacity bandwidth: 8
# Selected hotpluggable CPU: 1
# Expected tasks after offline: 7
# Starting 8 tasks at max-capacity bandwidth...
# Verifying additional task is rejected...
# Additional task correctly rejected: Device or resource busy
# Killing one task before CPU offline...
# Offlining CPU 1...
# Trying to respawn one task with CPU offline (expect reject)...
# Respawn correctly rejected: Device or resource busy
# Onlining CPU 1...
# Trying to respawn one task with CPU online (expect success)...
# SUCCESS: Hotplug accounting matched capacity-scaled expectations
# ok 5 bandwidth_hotplug_accounting #
# ===== END =====
From 788e3761ccfd2143f9ca0ee2981865bb5b60a6a7 Mon Sep 17 00:00:00 2001
From: Christian Loehle <[email protected]>
Date: Thu, 12 Mar 2026 11:28:25 +0000
Subject: [PATCH] selftests/sched: Add a hotplug-bandwidth selftest
Bandwidth is recalculated when CPU hotplugging, so add a test for:
-Spawn all capacity-computed tasks.
-Verify one more is rejected.
-Kill one task.
-Offline selected hotpluggable CPU.
-Verify respawn fails.
-Online CPU.
-Verify respawn succeeds.
Signed-off-by: Christian Loehle <[email protected]>
---
.../selftests/sched/deadline/bandwidth.c | 266 ++++++++++++++++++
1 file changed, 266 insertions(+)
diff --git a/tools/testing/selftests/sched/deadline/bandwidth.c b/tools/testing/selftests/sched/deadline/bandwidth.c
index f931b6bddac6..6dd5e61bb4fb 100644
--- a/tools/testing/selftests/sched/deadline/bandwidth.c
+++ b/tools/testing/selftests/sched/deadline/bandwidth.c
@@ -37,6 +37,7 @@ struct dl_bw_test_config {
unsigned long max_cpu_capacity;
int num_cpus;
int max_cpu;
+ int max_cpu_count;
int expected_tasks;
};
@@ -126,6 +127,104 @@ static double bw_scaled_to_percent(uint64_t scaled)
return (double)scaled * 100.0 / DL_BW_SCALE;
}
+static int bw_expected_tasks_for_totals(const struct dl_bw_test_config *cfg,
+ unsigned long total_cpu_capacity,
+ uint64_t total_server_bw_scaled)
+{
+ __uint128_t total_rt_bw;
+ __uint128_t total_server_bw;
+ __uint128_t total_available_bw;
+ __uint128_t task_bw_capacity;
+
+ total_rt_bw = (__uint128_t)total_cpu_capacity * cfg->rt_bw_scaled;
+ total_server_bw = (__uint128_t)cfg->max_cpu_capacity *
+ total_server_bw_scaled;
+ if (total_rt_bw <= total_server_bw)
+ return 0;
+
+ total_available_bw = total_rt_bw - total_server_bw;
+ task_bw_capacity = (__uint128_t)cfg->max_cpu_capacity *
+ cfg->task_bw_scaled;
+ if (!task_bw_capacity)
+ return 0;
+
+ return total_available_bw / task_bw_capacity;
+}
+
+static int bw_wait_cpu_state(int cpu, int online, int timeout_ms)
+{
+ int waited_ms = 0;
+
+ while (waited_ms < timeout_ms) {
+ if (dl_is_cpu_online(cpu) == online)
+ return 0;
+
+ usleep(10000); /* 10ms */
+ waited_ms += 10;
+ }
+
+ return -1;
+}
+
+static int bw_find_hotplug_cpu_for_offline_test(
+ const struct dl_bw_test_config *cfg,
+ int *expected_tasks_after_offline)
+{
+ int max_cpus;
+ int hotplug_count;
+ int *hotplug_cpus;
+ int i;
+ int selected_cpu = -1;
+
+ max_cpus = (int)sysconf(_SC_NPROCESSORS_CONF);
+ if (max_cpus <= 0)
+ return -1;
+
+ hotplug_cpus = calloc(max_cpus, sizeof(int));
+ if (!hotplug_cpus)
+ return -1;
+
+ hotplug_count = dl_get_hotpluggable_cpus(hotplug_cpus, max_cpus);
+ if (hotplug_count <= 0)
+ goto out;
+
+ for (i = 0; i < hotplug_count; i++) {
+ int cpu = hotplug_cpus[i];
+ unsigned long capacity;
+ uint64_t server_bw;
+ int expected_after;
+
+ if (!bw_is_cpu_online(cpu))
+ continue;
+
+ bw_get_cpu_capacity(cpu, &capacity);
+ server_bw = bw_get_server_bw_scaled(cpu);
+
+ if (cfg->total_cpu_capacity <= capacity)
+ continue;
+ if (cfg->total_server_bw_scaled < server_bw)
+ continue;
+
+ if (capacity == cfg->max_cpu_capacity && cfg->max_cpu_count == 1)
+ continue;
+
+ expected_after = bw_expected_tasks_for_totals(cfg,
+ cfg->total_cpu_capacity - capacity,
+ cfg->total_server_bw_scaled - server_bw);
+
+ if (expected_after == cfg->expected_tasks - 1) {
+ selected_cpu = cpu;
+ if (expected_tasks_after_offline)
+ *expected_tasks_after_offline = expected_after;
+ break;
+ }
+ }
+
+out:
+ free(hotplug_cpus);
+ return selected_cpu;
+}
+
static enum dl_test_status bw_prepare_test(struct dl_bw_test_config *cfg)
{
int cpu;
@@ -169,6 +268,9 @@ static enum dl_test_status bw_prepare_test(struct dl_bw_test_config *cfg)
cfg->max_cpu_capacity = capacity;
cfg->max_cpu_server_bw_scaled = server_bw;
cfg->max_cpu = cpu;
+ cfg->max_cpu_count = 1;
+ } else if (capacity == cfg->max_cpu_capacity) {
+ cfg->max_cpu_count++;
}
}
@@ -409,3 +511,167 @@ static struct dl_test test_bandwidth_overflow = {
.run = test_bandwidth_overflow_run,
};
REGISTER_DL_TEST(&test_bandwidth_overflow);
+
+/*
+ * Test: Capacity-scaled bandwidth accounting across CPU hotplug
+ *
+ * Verifies that admission accounting tracks CPU offline/online transitions on
+ * asymmetric systems using the same capacity-scaled math as admission:
+ *
+ * 1) Spawn all expected max-capacity tasks.
+ * 2) Verify one more task cannot be admitted.
+ * 3) Kill one task and offline one selected hotpluggable CPU.
+ * 4) Verify respawn fails while CPU is offline.
+ * 5) Online the CPU and verify respawn succeeds.
+ */
+static enum dl_test_status test_bandwidth_hotplug_accounting_run(void *ctx)
+{
+ struct dl_bw_test_config cfg;
+ int expected_after_offline = -1;
+ int hotplug_cpu;
+ pid_t *pids = NULL;
+ pid_t probe_pid;
+ int started = 0;
+ int i;
+ bool cpu_offlined = false;
+ enum dl_test_status ret = DL_TEST_FAIL;
+
+ DL_FAIL_IF(bw_prepare_test(&cfg) != DL_TEST_PASS,
+ "Failed to prepare bandwidth test parameters");
+ bw_print_test_config(&cfg);
+
+ if (cfg.expected_tasks < 2) {
+ printf(" Need at least 2 expected tasks for hotplug test (have %d)\n",
+ cfg.expected_tasks);
+ return DL_TEST_SKIP;
+ }
+
+ hotplug_cpu = bw_find_hotplug_cpu_for_offline_test(&cfg,
+ &expected_after_offline);
+ if (hotplug_cpu < 0) {
+ printf(" No suitable hotpluggable CPU found for exact offline/online transition\n");
+ return DL_TEST_SKIP;
+ }
+
+ printf(" Selected hotpluggable CPU: %d\n", hotplug_cpu);
+ printf(" Expected tasks after offline: %d\n", expected_after_offline);
+
+ pids = calloc(cfg.expected_tasks + 1, sizeof(pid_t));
+ DL_FAIL_IF(!pids, "Failed to allocate PID array");
+
+ printf(" Starting %d tasks at max-capacity bandwidth...\n",
+ cfg.expected_tasks);
+
+ for (i = 0; i < cfg.expected_tasks; i++) {
+ pids[i] = dl_create_cpuhog(cfg.runtime_ns, cfg.deadline_ns,
+ cfg.period_ns, 0);
+ if (pids[i] < 0) {
+ printf(" Task %d failed to start: %s\n",
+ i + 1, strerror(errno));
+ goto cleanup;
+ }
+ started++;
+ }
+
+ usleep(500000);
+
+ printf(" Verifying additional task is rejected...\n");
+ probe_pid = dl_create_cpuhog(cfg.runtime_ns, cfg.deadline_ns,
+ cfg.period_ns, 0);
+ if (probe_pid >= 0) {
+ printf(" ERROR: Additional task admitted at saturation\n");
+ dl_cleanup_cpuhog(probe_pid);
+ goto cleanup;
+ }
+
+ printf(" Additional task correctly rejected: %s\n", strerror(errno));
+
+ printf(" Killing one task before CPU offline...\n");
+ dl_cleanup_cpuhog(pids[started - 1]);
+ pids[started - 1] = 0;
+ started--;
+
+ usleep(200000);
+
+ printf(" Offlining CPU %d...\n", hotplug_cpu);
+ if (dl_cpu_offline(hotplug_cpu) < 0) {
+ DL_ERR("Failed to offline CPU %d: %s", hotplug_cpu, strerror(errno));
+ goto cleanup;
+ }
+
+ if (bw_wait_cpu_state(hotplug_cpu, 0, 3000) < 0) {
+ DL_ERR("CPU %d did not transition to offline state", hotplug_cpu);
+ goto cleanup;
+ }
+
+ cpu_offlined = true;
+ usleep(300000);
+
+ printf(" Trying to respawn one task with CPU offline (expect reject)...\n");
+ probe_pid = dl_create_cpuhog(cfg.runtime_ns, cfg.deadline_ns,
+ cfg.period_ns, 0);
+ if (probe_pid >= 0) {
+ printf(" ERROR: Respawn admitted while CPU is offline\n");
+ dl_cleanup_cpuhog(probe_pid);
+ goto cleanup;
+ }
+
+ printf(" Respawn correctly rejected: %s\n", strerror(errno));
+
+ printf(" Onlining CPU %d...\n", hotplug_cpu);
+ if (dl_cpu_online(hotplug_cpu) < 0) {
+ DL_ERR("Failed to online CPU %d: %s", hotplug_cpu, strerror(errno));
+ goto cleanup;
+ }
+
+ if (bw_wait_cpu_state(hotplug_cpu, 1, 5000) < 0) {
+ DL_ERR("CPU %d did not transition to online state", hotplug_cpu);
+ goto cleanup;
+ }
+
+ cpu_offlined = false;
+ usleep(300000);
+
+ printf(" Trying to respawn one task with CPU online (expect success)...\n");
+ pids[started] = dl_create_cpuhog(cfg.runtime_ns, cfg.deadline_ns,
+ cfg.period_ns, 0);
+ if (pids[started] < 0) {
+ DL_ERR("Respawn failed after CPU online: %s", strerror(errno));
+ goto cleanup;
+ }
+
+ if (!dl_is_deadline_task(pids[started])) {
+ DL_ERR("Respawned task is not running with SCHED_DEADLINE");
+ dl_cleanup_cpuhog(pids[started]);
+ pids[started] = 0;
+ goto cleanup;
+ }
+
+ started++;
+ printf(" SUCCESS: Hotplug accounting matched capacity-scaled expectations\n");
+ ret = DL_TEST_PASS;
+
+cleanup:
+ for (i = 0; i < started; i++) {
+ if (pids && pids[i] > 0)
+ dl_cleanup_cpuhog(pids[i]);
+ }
+
+ if (cpu_offlined) {
+ if (dl_cpu_online(hotplug_cpu) < 0)
+ printf(" WARN: Failed to restore CPU %d online: %s\n",
+ hotplug_cpu, strerror(errno));
+ else
+ bw_wait_cpu_state(hotplug_cpu, 1, 5000);
+ }
+
+ free(pids);
+ return ret;
+}
+
+static struct dl_test test_bandwidth_hotplug_accounting = {
+ .name = "bandwidth_hotplug_accounting",
+ .description = "Verify capacity-scaled bandwidth accounting across CPU hotplug",
+ .run = test_bandwidth_hotplug_accounting_run,
+};
+REGISTER_DL_TEST(&test_bandwidth_hotplug_accounting);
--
2.34.1