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

Reply via email to