Skip to contentSkip to conversation

Backport "sched cpuset: Bring back cpuset_mutex"

PATCH In review Luiz Capitulino 2 participants 9 messages Aug 31, 2023 Stable branch

lore
Share & tools
lore
#0 lore raw

cover Backport "sched cpuset: Bring back cpuset_mutex"

Hi,

When using KVM on systems that require iTLB multihit mitigation enabled[1], we're observing very high latency (70ms+) in KVM_CREATE_VM ioctl() in 6.1 kernel in comparison to older stable kernels such as 5.10. This is true even when using favordynmods mount option.

We debugged this down to the cpuset controller trying to acquire cpuset_rwsem in cpuset_can_attach(). This happens because KVM creates a worker thread which calls cgroup_attach_task_all() during KVM_CREATE_VM. I don't know if favordynmods is supposed to cover this case or not, but removing cpuset_rwsem certainly solves the issue.

For the backport I tried to pick as many dependent commits as required to avoid conflicts. I would highly appreciate review from cgroup people.

Tests performed:
* Measured latency in KVM_CREATE_VM ioctl(), it goes down to less than 1ms
* Ran the cgroup kselftest tests, got same results with or without this series
* However, some tests such as test_memcontrol and test_kmem are failing in 6.1. This probably needs to be looked at
* To make test_cpuset_prs.sh work, I had to increase the timeout on line 592 to 1 second. With this change, the test runs and passes
* I run our downstream test suite against our downstream 6.1 kernel with this series applied, it passed

[1] For the case where the CPU is not vulnerable to iTLB multihit we can simply disable the iTLB multihit mitigation in KVM which avoids this whole situation. Disabling the mitigation is possible since upstream commit 0b210faf337 which I plan to backport soon

Daniel Vacek (1):
cgroup/cpuset: no need to explicitly init a global static variable

Juri Lelli (1):
sched/cpuset: Bring back cpuset_mutex

Waiman Long (3):
cgroup/cpuset: Optimize cpuset_attach() on v2 cgroup/cpuset: Skip task update if hotplug doesn't affect current
cpuset
cgroup/cpuset: Include offline CPUs when tasks' cpumasks in top_cpuset
are updated

 include/linux/cpuset.h |   8 +-
 kernel/cgroup/cpuset.c | 211 +++++++++++++++++++++++------------------
 kernel/sched/core.c    |  22 +++--
 3 files changed, 139 insertions(+), 102 deletions(-)
Signature · 1 line
2.40.1
#1 lore raw

PATCH 1/5 cgroup/cpuset: Optimize cpuset_attach() on v2

From: Waiman Long <[email protected]>

Commit 7fd4da9c1584be97ffbc40e600a19cb469fd4e78 upstream.

It was found that with the default hierarchy, enabling cpuset in the child cgroups can trigger a cpuset_attach() call in each of the child cgroups that have tasks with no change in effective cpus and mems. If there are many processes in those child cgroups, it will burn quite a lot of cpu cycles iterating all the tasks without doing useful work.

Optimizing this case by comparing between the old and new cpusets and skip useless update if there is no change in effective cpus and mems. Also mems_allowed are less likely to be changed than cpus_allowed. So skip changing mm if there is no change in effective_mems and CS_MEMORY_MIGRATE is not set.

By inserting some instrumentation code and running a simple command in a container 200 times in a cgroup v2 system, it was found that all the cpuset_attach() calls are skipped (401 times in total) as there was no change in effective cpus and mems.

---
 kernel/cgroup/cpuset.c | 24 +++++++++++++++++++++++-
 1 file changed, 23 insertions(+), 1 deletion(-)
kernel/cgroup/cpuset.c +23 -1
index e276db722845..0496b88fcd63 100644
@@ -2553,12 +2553,28 @@ static void cpuset_attach(struct cgroup_taskset *tset)
 	struct cgroup_subsys_state *css;
 	struct cpuset *cs;
 	struct cpuset *oldcs = cpuset_attach_old_cs;
+	bool cpus_updated, mems_updated;
 
 	cgroup_taskset_first(tset, &css);
 	cs = css_cs(css);
 
 	lockdep_assert_cpus_held();	/* see cgroup_attach_lock() */
 	percpu_down_write(&cpuset_rwsem);
+	cpus_updated = !cpumask_equal(cs->effective_cpus,
+				      oldcs->effective_cpus);
+	mems_updated = !nodes_equal(cs->effective_mems, oldcs->effective_mems);
+
+	/*
+	 * In the default hierarchy, enabling cpuset in the child cgroups
+	 * will trigger a number of cpuset_attach() calls with no change
+	 * in effective cpus and mems. In that case, we can optimize out
+	 * by skipping the task iteration and update.
+	 */
+	if (cgroup_subsys_on_dfl(cpuset_cgrp_subsys) &&
+	    !cpus_updated && !mems_updated) {
+		cpuset_attach_nodemask_to = cs->effective_mems;
+		goto out;
+	}
 
 	guarantee_online_mems(cs, &cpuset_attach_nodemask_to);
 
@@ -2567,9 +2583,14 @@ static void cpuset_attach(struct cgroup_taskset *tset)
 
 	/*
 	 * Change mm for all threadgroup leaders. This is expensive and may
-	 * sleep and should be moved outside migration path proper.
+	 * sleep and should be moved outside migration path proper. Skip it
+	 * if there is no change in effective_mems and CS_MEMORY_MIGRATE is
+	 * not set.
 	 */
 	cpuset_attach_nodemask_to = cs->effective_mems;
+	if (!is_memory_migrate(cs) && !mems_updated)
+		goto out;
+
 	cgroup_taskset_for_each_leader(leader, css, tset) {
 		struct mm_struct *mm = get_task_mm(leader);
 
@@ -2592,6 +2613,7 @@ static void cpuset_attach(struct cgroup_taskset *tset)
 		}
 	}
 
+out:
 	cs->old_mems_allowed = cpuset_attach_nodemask_to;
 
 	cs->attach_in_progress--;
Signature · 1 line
2.40.1
#2 lore raw

PATCH 2/5 cgroup/cpuset: no need to explicitly init a global static variable

In reply to Luiz Capitulino

From: Daniel Vacek <[email protected]>

Commit 21786e5cb375a1e58a9175fee423e1d7f892d965 upstream.

cpuset_rwsem is a static variable defined with DEFINE_STATIC_PERCPU_RWSEM(). It's initialized at build time and so there's no need for explicit runtime init leaking one percpu int.

---
 kernel/cgroup/cpuset.c | 2 --
 1 file changed, 2 deletions(-)
kernel/cgroup/cpuset.c +0 -2
index 0496b88fcd63..4baae4b3c9a0 100644
@@ -3391,8 +3391,6 @@ struct cgroup_subsys cpuset_cgrp_subsys = {
 
 int __init cpuset_init(void)
 {
-	BUG_ON(percpu_init_rwsem(&cpuset_rwsem));
-
 	BUG_ON(!alloc_cpumask_var(&top_cpuset.cpus_allowed, GFP_KERNEL));
 	BUG_ON(!alloc_cpumask_var(&top_cpuset.effective_cpus, GFP_KERNEL));
 	BUG_ON(!zalloc_cpumask_var(&top_cpuset.subparts_cpus, GFP_KERNEL));
Signature · 1 line
2.40.1
#3 lore raw

PATCH 3/5 cgroup/cpuset: Skip task update if hotplug doesn't affect current cpuset

In reply to Luiz Capitulino

From: Waiman Long <[email protected]>

Commit df59b72cd8fb8ca00301f47e65853efed195d23f upstream.

If a hotplug event doesn't affect the current cpuset, there is no point to call hotplug_update_tasks() or hotplug_update_tasks_legacy(). So just skip it.

---
 kernel/cgroup/cpuset.c | 3 +++
 1 file changed, 3 insertions(+)
kernel/cgroup/cpuset.c +3 -0
index 4baae4b3c9a0..8664b9c1edc8 100644
@@ -3613,6 +3613,8 @@ static void cpuset_hotplug_update_tasks(struct cpuset *cs, struct tmpmasks *tmp)
 update_tasks:
 	cpus_updated = !cpumask_equal(&new_cpus, cs->effective_cpus);
 	mems_updated = !nodes_equal(new_mems, cs->effective_mems);
+	if (!cpus_updated && !mems_updated)
+		goto unlock;	/* Hotplug doesn't affect this cpuset */
 
 	if (mems_updated)
 		check_insane_mems_config(&new_mems);
@@ -3624,6 +3626,7 @@ static void cpuset_hotplug_update_tasks(struct cpuset *cs, struct tmpmasks *tmp)
 		hotplug_update_tasks_legacy(cs, &new_cpus, &new_mems,
 					    cpus_updated, mems_updated);
 
+unlock:
 	percpu_up_write(&cpuset_rwsem);
 }
 
Signature · 1 line
2.40.1
#4 lore raw

PATCH 4/5 cgroup/cpuset: Include offline CPUs when tasks' cpumasks in top_cpuset are updated

In reply to Luiz Capitulino

From: Waiman Long <[email protected]>

Commit 6667439f51c446fead5d991ff49b842a811a6195 upstream.

Similar to commit 3fb906e7fabb ("group/cpuset: Don't filter offline CPUs in cpuset_cpus_allowed() for top cpuset tasks"), the whole set of possible CPUs including offline ones should be used for setting cpumasks for tasks in the top cpuset when a cpuset partition is modified as the hotplug code won't update cpumasks for tasks in the top cpuset when CPUs become online or offline.

---
 kernel/cgroup/cpuset.c | 23 ++++++++++++++---------
 1 file changed, 14 insertions(+), 9 deletions(-)
kernel/cgroup/cpuset.c +14 -9
index 8664b9c1edc8..91c4256a3da4 100644
@@ -1209,7 +1209,9 @@ void rebuild_sched_domains(void)
  *
  * Iterate through each task of @cs updating its cpus_allowed to the
  * effective cpuset's.  As this function is called with cpuset_rwsem held,
- * cpuset membership stays stable.
+ * cpuset membership stays stable. For top_cpuset, task_cpu_possible_mask()
+ * is used instead of effective_cpus to make sure all offline CPUs are also
+ * included as hotplug code won't update cpumasks for tasks in top_cpuset.
  */
 static void update_tasks_cpumask(struct cpuset *cs, struct cpumask *new_cpus)
 {
@@ -1219,15 +1221,18 @@ static void update_tasks_cpumask(struct cpuset *cs, struct cpumask *new_cpus)
 
 	css_task_iter_start(&cs->css, 0, &it);
 	while ((task = css_task_iter_next(&it))) {
-		/*
-		 * Percpu kthreads in top_cpuset are ignored
-		 */
-		if (top_cs && (task->flags & PF_KTHREAD) &&
-		    kthread_is_per_cpu(task))
-			continue;
+		const struct cpumask *possible_mask = task_cpu_possible_mask(task);
 
-		cpumask_and(new_cpus, cs->effective_cpus,
-			    task_cpu_possible_mask(task));
+		if (top_cs) {
+			/*
+			 * Percpu kthreads in top_cpuset are ignored
+			 */
+			if ((task->flags & PF_KTHREAD) && kthread_is_per_cpu(task))
+				continue;
+			cpumask_andnot(new_cpus, possible_mask, cs->subparts_cpus);
+		} else {
+			cpumask_and(new_cpus, possible_mask, cs->effective_cpus);
+		}
 		set_cpus_allowed_ptr(task, new_cpus);
 	}
 	css_task_iter_end(&it);
Signature · 1 line
2.40.1
#5 lore raw

PATCH 5/5 sched/cpuset: Bring back cpuset_mutex

In reply to Luiz Capitulino

From: Juri Lelli <[email protected]>

Commit 111cd11bbc54850f24191c52ff217da88a5e639b upstream.

Turns out percpu_cpuset_rwsem - commit 1243dc518c9d ("cgroup/cpuset:
Convert cpuset_mutex to percpu_rwsem") - wasn't such a brilliant idea, as it has been reported to cause slowdowns in workloads that need to change cpuset configuration frequently and it is also not implementing priority inheritance (which causes troubles with realtime workloads).

Convert percpu_cpuset_rwsem back to regular cpuset_mutex. Also grab it only for SCHED_DEADLINE tasks (other policies don't care about stable cpusets anyway).

---
 include/linux/cpuset.h |   8 +--
 kernel/cgroup/cpuset.c | 159 +++++++++++++++++++++--------------------
 kernel/sched/core.c    |  22 ++++--
 3 files changed, 99 insertions(+), 90 deletions(-)
include/linux/cpuset.h +4 -4
index d58e0476ee8e..355f796c5f07 100644
@@ -71,8 +71,8 @@ extern void cpuset_init_smp(void);
 extern void cpuset_force_rebuild(void);
 extern void cpuset_update_active_cpus(void);
 extern void cpuset_wait_for_hotplug(void);
-extern void cpuset_read_lock(void);
-extern void cpuset_read_unlock(void);
+extern void cpuset_lock(void);
+extern void cpuset_unlock(void);
 extern void cpuset_cpus_allowed(struct task_struct *p, struct cpumask *mask);
 extern bool cpuset_cpus_allowed_fallback(struct task_struct *p);
 extern nodemask_t cpuset_mems_allowed(struct task_struct *p);
@@ -196,8 +196,8 @@ static inline void cpuset_update_active_cpus(void)
 
 static inline void cpuset_wait_for_hotplug(void) { }
 
-static inline void cpuset_read_lock(void) { }
-static inline void cpuset_read_unlock(void) { }
+static inline void cpuset_lock(void) { }
+static inline void cpuset_unlock(void) { }
 
 static inline void cpuset_cpus_allowed(struct task_struct *p,
 				       struct cpumask *mask)
kernel/cgroup/cpuset.c +80 -79

535 lines not on this page · raw message

kernel/sched/core.c +15 -7
index b23dcbeacdf3..8a8dbb2dac03 100644
@@ -7475,6 +7475,7 @@ static int __sched_setscheduler(struct task_struct *p,
 	int reset_on_fork;
 	int queue_flags = DEQUEUE_SAVE | DEQUEUE_MOVE | DEQUEUE_NOCLOCK;
 	struct rq *rq;
+	bool cpuset_locked = false;
 
 	/* The pi code expects interrupts enabled */
 	BUG_ON(pi && in_interrupt());
@@ -7524,8 +7525,14 @@ static int __sched_setscheduler(struct task_struct *p,
 			return retval;
 	}
 
-	if (pi)
-		cpuset_read_lock();
+	/*
+	 * SCHED_DEADLINE bandwidth accounting relies on stable cpusets
+	 * information.
+	 */
+	if (dl_policy(policy) || dl_policy(p->policy)) {
+		cpuset_locked = true;
+		cpuset_lock();
+	}
 
 	/*
 	 * Make sure no PI-waiters arrive (or leave) while we are
@@ -7601,8 +7608,8 @@ static int __sched_setscheduler(struct task_struct *p,
 	if (unlikely(oldpolicy != -1 && oldpolicy != p->policy)) {
 		policy = oldpolicy = -1;
 		task_rq_unlock(rq, p, &rf);
-		if (pi)
-			cpuset_read_unlock();
+		if (cpuset_locked)
+			cpuset_unlock();
 		goto recheck;
 	}
 
@@ -7669,7 +7676,8 @@ static int __sched_setscheduler(struct task_struct *p,
 	task_rq_unlock(rq, p, &rf);
 
 	if (pi) {
-		cpuset_read_unlock();
+		if (cpuset_locked)
+			cpuset_unlock();
 		rt_mutex_adjust_pi(p);
 	}
 
@@ -7681,8 +7689,8 @@ static int __sched_setscheduler(struct task_struct *p,
 
 unlock:
 	task_rq_unlock(rq, p, &rf);
-	if (pi)
-		cpuset_read_unlock();
+	if (cpuset_locked)
+		cpuset_unlock();
 	return retval;
 }
 
Signature · 1 line
2.40.1
maintainer
#6 lore raw
In reply to Luiz Capitulino
Quoted 26 lines · Luiz Capitulino

Hi,

When using KVM on systems that require iTLB multihit mitigation enabled[1], we're observing very high latency (70ms+) in KVM_CREATE_VM ioctl() in 6.1 kernel in comparison to older stable kernels such as 5.10. This is true even when using favordynmods mount option.

We debugged this down to the cpuset controller trying to acquire cpuset_rwsem in cpuset_can_attach(). This happens because KVM creates a worker thread which calls cgroup_attach_task_all() during KVM_CREATE_VM. I don't know if favordynmods is supposed to cover this case or not, but removing cpuset_rwsem certainly solves the issue.

For the backport I tried to pick as many dependent commits as required to avoid conflicts. I would highly appreciate review from cgroup people.

Tests performed:
* Measured latency in KVM_CREATE_VM ioctl(), it goes down to less than 1ms
* Ran the cgroup kselftest tests, got same results with or without this series
* However, some tests such as test_memcontrol and test_kmem are failing in 6.1. This probably needs to be looked at
* To make test_cpuset_prs.sh work, I had to increase the timeout on line 592 to 1 second. With this change, the test runs and passes
* I run our downstream test suite against our downstream 6.1 kernel with this series applied, it passed

Quoted 4 lines

[1] For the case where the CPU is not vulnerable to iTLB multihit we can simply disable the iTLB multihit mitigation in KVM which avoids this whole situation. Disabling the mitigation is possible since upstream commit 0b210faf337 which I plan to backport soon

Please try 6.1.50, I think you will find that this issue is resolved there, right?

if not, please rebase your series on top of that, as obviously, it does not still apply anymore.

thanks,

greg k-h

#7 lore raw
Quoted 30 lines · Greg KH
Quoted 26 lines · Luiz Capitulino

Hi,

When using KVM on systems that require iTLB multihit mitigation enabled[1], we're observing very high latency (70ms+) in KVM_CREATE_VM ioctl() in 6.1 kernel in comparison to older stable kernels such as 5.10. This is true even when using favordynmods mount option.

We debugged this down to the cpuset controller trying to acquire cpuset_rwsem in cpuset_can_attach(). This happens because KVM creates a worker thread which calls cgroup_attach_task_all() during KVM_CREATE_VM. I don't know if favordynmods is supposed to cover this case or not, but removing cpuset_rwsem certainly solves the issue.

For the backport I tried to pick as many dependent commits as required to avoid conflicts. I would highly appreciate review from cgroup people.

Tests performed:
* Measured latency in KVM_CREATE_VM ioctl(), it goes down to less than 1ms
* Ran the cgroup kselftest tests, got same results with or without this series
* However, some tests such as test_memcontrol and test_kmem are failing in 6.1. This probably needs to be looked at
* To make test_cpuset_prs.sh work, I had to increase the timeout on line 592 to 1 second. With this change, the test runs and passes
* I run our downstream test suite against our downstream 6.1 kernel with this series applied, it passed

Quoted 7 lines
Quoted 4 lines

[1] For the case where the CPU is not vulnerable to iTLB multihit we can simply disable the iTLB multihit mitigation in KVM which avoids this whole situation. Disabling the mitigation is possible since upstream commit 0b210faf337 which I plan to backport soon

Please try 6.1.50, I think you will find that this issue is resolved there, right?

It should, since the most important is dropping cpuset_rwsem from cpuset. I'll double check to be sure.

Thank you very much for the quick reply. I queued this series before my vacation and didn't check back before sending it today.

- Luiz

Quoted 7 lines

if not, please rebase your series on top of that, as obviously, it does not still apply anymore.

thanks,

greg k-h

#8 lore raw
Quoted 39 lines · Luiz Capitulino
Quoted 35 lines · Greg KH
Quoted 30 lines · Luiz Capitulino

Hi,

When using KVM on systems that require iTLB multihit mitigation enabled[1], we're observing very high latency (70ms+) in KVM_CREATE_VM ioctl() in 6.1 kernel in comparison to older stable kernels such as 5.10. This is true even when using favordynmods mount option.

We debugged this down to the cpuset controller trying to acquire cpuset_rwsem in cpuset_can_attach(). This happens because KVM creates a worker thread which calls cgroup_attach_task_all() during KVM_CREATE_VM. I don't know if favordynmods is supposed to cover this case or not, but removing cpuset_rwsem certainly solves the issue.

For the backport I tried to pick as many dependent commits as required to avoid conflicts. I would highly appreciate review from cgroup people.

Tests performed:
  * Measured latency in KVM_CREATE_VM ioctl(), it goes down to less than 1ms
  * Ran the cgroup kselftest tests, got same results with or without this series
     * However, some tests such as test_memcontrol and test_kmem are failing in 6.1. This probably needs to be looked at
     * To make test_cpuset_prs.sh work, I had to increase the timeout on line 592 to 1 second. With this change, the test runs and passes
  * I run our downstream test suite against our downstream 6.1 kernel with this series applied, it passed

  [1] For the case where the CPU is not vulnerable to iTLB multihit we can simply disable the iTLB multihit mitigation in KVM which avoids this whole situation. Disabling the mitigation is possible since upstream commit 0b210faf337 which I plan to backport soon

Quoted 5 lines
Quoted 2 lines

Please try 6.1.50, I think you will find that this issue is resolved there, right?

It should, since the most important is dropping cpuset_rwsem from cpuset. I'll double check to be sure.

Quick test passed. We'll be doing more testing in the next days and I'll report if there are any issues.

- Luiz

Quoted 13 lines

Thank you very much for the quick reply. I queued this series before my vacation and didn't check back before sending it today.

- Luiz

Quoted 7 lines

if not, please rebase your series on top of that, as obviously, it does not still apply anymore.

thanks,

greg k-h