Linux-HyperV List
 help / color / mirror / Atom feed
* [PATCH] x86/hyperv: Account for lazy mm_cpumask maintenance
@ 2026-10-29  6:42 Naman Jain
  2026-10-30  2:39 ` Michael Kelley
  0 siblings, 1 reply; 4+ messages in thread
From: Naman Jain @ 2026-10-29  6:42 UTC (permalink / raw)
  To: haiyangz, wei.liu, decui, tglx, mingo, bp, dave.hansen, x86, luto,
	peterz
  Cc: hpa, riel, mhklinux, linux-hyperv, linux-kernel

CPUs no longer running an mm may remain in mm_cpumask(). Hyper-V treats
these CPUs as flush targets and bypasses the native callbacks that trim
the mask. A single-threaded process can therefore keep issuing
all-processor flushes after visiting all CPUs and returning to one CPU.

Share the native target predicate and filter the mask before building
either hypercall format or selecting HV_FLUSH_ALL_PROCESSORS. Preserve
lazy-TLB exclusion and unfiltered page-table-freeing flushes, and use
native callbacks for periodic trimming and ASID transitions.

When no remote targets remain, request immediate trimming through native
callbacks so stale memberships do not unnecessarily prevent later
invalidations from using the local fast path. Leave lazy CPU-mask
maintenance during context switches unchanged.

Fixes: 209954cbc7d0 ("x86/mm/tlb: Update mm_cpumask lazily")
Signed-off-by: Naman Jain <namjain@linux.microsoft.com>
---
On a 32-vCPU Hyper-V guest, after a single-threaded process visited all
CPUs and returned to one CPU, the reproducer's median time per iteration
(two mprotect() calls and a write) dropped from 17.235 us to 1.622 us,
a 90.6% reduction. This could be more on higher vCPU VMs.

 arch/x86/hyperv/mmu.c           | 62 +++++++++++++++++++++------------
 arch/x86/include/asm/tlbflush.h |  6 ++++
 arch/x86/mm/tlb.c               |  8 +++--
 3 files changed, 52 insertions(+), 24 deletions(-)

diff --git a/arch/x86/hyperv/mmu.c b/arch/x86/hyperv/mmu.c
index cfcb60468b01c..128ad3ce8c24c 100644
--- a/arch/x86/hyperv/mmu.c
+++ b/arch/x86/hyperv/mmu.c
@@ -1,6 +1,7 @@
 #define pr_fmt(fmt)  "Hyper-V: " fmt
 
 #include <linux/log2.h>
+#include <linux/percpu.h>
 #include <linux/slab.h>
 #include <linux/types.h>
 
@@ -16,6 +17,8 @@
 /* Each gva in gva_list encodes up to 4096 pages to flush */
 #define HV_TLB_FLUSH_UNIT (4096 * PAGE_SIZE)
 
+static DEFINE_PER_CPU(struct cpumask, hyperv_tlb_flush_mask);
+
 static u64 hyperv_flush_tlb_others_ex(const struct cpumask *cpus,
 				      const struct flush_tlb_info *info);
 
@@ -51,23 +54,23 @@ static inline int fill_gva_list(u64 gva_list[], int offset,
 	return gva_n - offset;
 }
 
-static bool cpu_is_lazy(int cpu)
-{
-	return per_cpu(cpu_tlbstate_shared.is_lazy, cpu);
-}
-
 static void hyperv_flush_tlb_multi(const struct cpumask *cpus,
 				   const struct flush_tlb_info *info)
 {
 	int cpu, vcpu, gva_n, max_gvas;
 	struct hv_tlb_flush *flush;
+	struct cpumask *flush_cpus;
 	u64 status;
 	unsigned long flags;
-	bool do_lazy = !info->freed_tables;
 
 	trace_hyperv_mmu_flush_tlb_multi(cpus, info);
 
-	if (!hv_hypercall_pg)
+	/*
+	 * Hypercalls bypass the callbacks that trim mm_cpumask() and complete
+	 * ASID transitions.
+	 */
+	if (!hv_hypercall_pg || info->trim_cpumask ||
+	    mm_in_asid_transition(info->mm))
 		goto do_native;
 
 	local_irq_save(flags);
@@ -79,6 +82,30 @@ static void hyperv_flush_tlb_multi(const struct cpumask *cpus,
 		goto do_native;
 	}
 
+	flush_cpus = this_cpu_ptr(&hyperv_tlb_flush_mask);
+	if (info->freed_tables) {
+		cpumask_copy(flush_cpus, cpus);
+	} else {
+		cpumask_clear(flush_cpus);
+		for_each_cpu(cpu, cpus) {
+			if (cpu_needs_tlb_flush(cpu, info))
+				__cpumask_set_cpu(cpu, flush_cpus);
+		}
+	}
+
+	/*
+	 * An effectively local mm should not keep taking the remote path until
+	 * the next periodic trim. Let the native callbacks prune stale bits.
+	 */
+	if (cpumask_any_but(flush_cpus, smp_processor_id()) >= nr_cpu_ids) {
+		struct flush_tlb_info local_info = *info;
+
+		local_info.trim_cpumask = 1;
+		local_irq_restore(flags);
+		native_flush_tlb_multi(cpus, &local_info);
+		return;
+	}
+
 	if (info->mm) {
 		/*
 		 * AddressSpace argument must match the CR3 with PCID bits
@@ -93,7 +120,7 @@ static void hyperv_flush_tlb_multi(const struct cpumask *cpus,
 	}
 
 	flush->processor_mask = 0;
-	if (cpumask_equal(cpus, cpu_present_mask)) {
+	if (cpumask_equal(flush_cpus, cpu_present_mask)) {
 		flush->flags |= HV_FLUSH_ALL_PROCESSORS;
 	} else {
 		/*
@@ -107,14 +134,12 @@ static void hyperv_flush_tlb_multi(const struct cpumask *cpus,
 		 * must. We will also check all VP numbers when walking the
 		 * supplied CPU set to remain correct in all cases.
 		 */
-		cpu = cpumask_last(cpus);
+		cpu = cpumask_last(flush_cpus);
 
 		if (cpu < nr_cpumask_bits && hv_cpu_number_to_vp_number(cpu) >= 64)
 			goto do_ex_hypercall;
 
-		for_each_cpu(cpu, cpus) {
-			if (do_lazy && cpu_is_lazy(cpu))
-				continue;
+		for_each_cpu(cpu, flush_cpus) {
 			vcpu = hv_cpu_number_to_vp_number(cpu);
 			if (vcpu == VP_INVAL) {
 				local_irq_restore(flags);
@@ -127,12 +152,6 @@ static void hyperv_flush_tlb_multi(const struct cpumask *cpus,
 			__set_bit(vcpu, (unsigned long *)
 				  &flush->processor_mask);
 		}
-
-		/* nothing to flush if 'processor_mask' ends up being empty */
-		if (!flush->processor_mask) {
-			local_irq_restore(flags);
-			return;
-		}
 	}
 
 	/*
@@ -158,7 +177,7 @@ static void hyperv_flush_tlb_multi(const struct cpumask *cpus,
 	goto check_status;
 
 do_ex_hypercall:
-	status = hyperv_flush_tlb_others_ex(cpus, info);
+	status = hyperv_flush_tlb_others_ex(flush_cpus, info);
 
 check_status:
 	local_irq_restore(flags);
@@ -197,9 +216,8 @@ static u64 hyperv_flush_tlb_others_ex(const struct cpumask *cpus,
 	flush->hv_vp_set.valid_bank_mask = 0;
 
 	flush->hv_vp_set.format = HV_GENERIC_SET_SPARSE_4K;
-	nr_bank = cpumask_to_vpset_skip(&flush->hv_vp_set, cpus,
-			info->freed_tables ? NULL : cpu_is_lazy);
-	if (nr_bank < 0)
+	nr_bank = cpumask_to_vpset(&flush->hv_vp_set, cpus);
+	if (nr_bank <= 0)
 		return HV_STATUS_INVALID_PARAMETER;
 
 	/*
diff --git a/arch/x86/include/asm/tlbflush.h b/arch/x86/include/asm/tlbflush.h
index 70098d448e999..e1e05cc867d5a 100644
--- a/arch/x86/include/asm/tlbflush.h
+++ b/arch/x86/include/asm/tlbflush.h
@@ -264,6 +264,12 @@ void flush_tlb_one_kernel(unsigned long addr);
 void flush_tlb_multi(const struct cpumask *cpumask,
 		      const struct flush_tlb_info *info);
 
+/*
+ * Do not use for filtering when freeing page tables or during global ASID
+ * transitions.
+ */
+bool cpu_needs_tlb_flush(int cpu, const struct flush_tlb_info *info);
+
 static inline bool is_dyn_asid(u16 asid)
 {
 	return asid < TLB_NR_DYN_ASIDS;
diff --git a/arch/x86/mm/tlb.c b/arch/x86/mm/tlb.c
index 0c4da320831cb..f2f40928d56a4 100644
--- a/arch/x86/mm/tlb.c
+++ b/arch/x86/mm/tlb.c
@@ -1273,10 +1273,9 @@ static void flush_tlb_func(void *info)
 			nr_invalidate);
 }
 
-static bool should_flush_tlb(int cpu, void *data)
+bool cpu_needs_tlb_flush(int cpu, const struct flush_tlb_info *info)
 {
 	struct mm_struct *loaded_mm = per_cpu(cpu_tlbstate.loaded_mm, cpu);
-	struct flush_tlb_info *info = data;
 
 	/*
 	 * Order the 'loaded_mm' and 'is_lazy' against their
@@ -1311,6 +1310,11 @@ static bool should_flush_tlb(int cpu, void *data)
 	return false;
 }
 
+static bool should_flush_tlb(int cpu, void *data)
+{
+	return cpu_needs_tlb_flush(cpu, data);
+}
+
 static bool should_trim_cpumask(struct mm_struct *mm)
 {
 	if (time_after(jiffies, READ_ONCE(mm->context.next_trim_cpumask))) {
-- 
2.43.0

^ permalink raw reply related	[flat|nested] 4+ messages in thread

* RE: [PATCH] x86/hyperv: Account for lazy mm_cpumask maintenance
  2026-10-29  6:42 [PATCH] x86/hyperv: Account for lazy mm_cpumask maintenance Naman Jain
@ 2026-10-30  2:39 ` Michael Kelley
  2026-10-30  4:00   ` Naman Jain
  0 siblings, 1 reply; 4+ messages in thread
From: Michael Kelley @ 2026-10-30  2:39 UTC (permalink / raw)
  To: Naman Jain, haiyangz@microsoft.com, wei.liu@kernel.org,
	decui@microsoft.com, tglx@kernel.org, mingo@redhat.com,
	bp@alien8.de, dave.hansen@linux.intel.com, x86@kernel.org,
	luto@kernel.org, peterz@infradead.org
  Cc: hpa@zytor.com, riel@surriel.com, linux-hyperv@vger.kernel.org,
	linux-kernel@vger.kernel.org

From: Naman Jain <namjain@linux.microsoft.com> Sent: Wednesday, October 28, 2026 11:43 PM
> 
> CPUs no longer running an mm may remain in mm_cpumask(). Hyper-V treats
> these CPUs as flush targets and bypasses the native callbacks that trim
> the mask. A single-threaded process can therefore keep issuing
> all-processor flushes after visiting all CPUs and returning to one CPU.
> 
> Share the native target predicate and filter the mask before building
> either hypercall format or selecting HV_FLUSH_ALL_PROCESSORS. Preserve
> lazy-TLB exclusion and unfiltered page-table-freeing flushes, and use
> native callbacks for periodic trimming and ASID transitions.
> 
> When no remote targets remain, request immediate trimming through native
> callbacks so stale memberships do not unnecessarily prevent later
> invalidations from using the local fast path. Leave lazy CPU-mask
> maintenance during context switches unchanged.
> 
> Fixes: 209954cbc7d0 ("x86/mm/tlb: Update mm_cpumask lazily")
> Signed-off-by: Naman Jain <namjain@linux.microsoft.com>

I've read the commit message but haven't yet delved into the code for
this patch. But the commit message feels like it is missing some context,
particularly with the "Fixes:" tag.

I recall the last significant change to hyperv_flush_tlb_multi(), which takes
CPUs in lazy tlb mode into account, and thereby avoids unnecessary flushing
(commit 493cc07385cf, made in March 2023). It was a case where the
native x86 flush code had gotten smarter, and the Hyper-V PV flush
functions had not been updated to be smarter in the same way. Against
that context, what is the situation here? Commit 209954cbc7d0 came a
year and a half later in November 2024. Did it make things worse for the
Hyper-V PV flush functions, and hence is deemed to have "broken"
them? Or is it another native flush improvement that the Hyper-V PV
flush functions just don't take advantage of?

I'm also curious how the issue was noticed. Was there a real-world
performance problem, or is it a case of looking at the code and
noticing a native improvement that hadn't been applied to the
Hyper-V PV flush functions? The scenario of a single-threaded process
visiting all CPUs and then returning to one CPU seems contrived,
particularly as CPU counts get large. It may illustrate the improvement
that is possible, but to me seems unlikely to be how the problem was
originally noticed.

I intended to review the code carefully, but the additional context
would be helpful in doing that review.

Michael

> ---
> On a 32-vCPU Hyper-V guest, after a single-threaded process visited all
> CPUs and returned to one CPU, the reproducer's median time per iteration
> (two mprotect() calls and a write) dropped from 17.235 us to 1.622 us,
> a 90.6% reduction. This could be more on higher vCPU VMs.
> 
>  arch/x86/hyperv/mmu.c           | 62 +++++++++++++++++++++------------
>  arch/x86/include/asm/tlbflush.h |  6 ++++
>  arch/x86/mm/tlb.c               |  8 +++--
>  3 files changed, 52 insertions(+), 24 deletions(-)
> 
> diff --git a/arch/x86/hyperv/mmu.c b/arch/x86/hyperv/mmu.c
> index cfcb60468b01c..128ad3ce8c24c 100644
> --- a/arch/x86/hyperv/mmu.c
> +++ b/arch/x86/hyperv/mmu.c
> @@ -1,6 +1,7 @@
>  #define pr_fmt(fmt)  "Hyper-V: " fmt
> 
>  #include <linux/log2.h>
> +#include <linux/percpu.h>
>  #include <linux/slab.h>
>  #include <linux/types.h>
> 
> @@ -16,6 +17,8 @@
>  /* Each gva in gva_list encodes up to 4096 pages to flush */
>  #define HV_TLB_FLUSH_UNIT (4096 * PAGE_SIZE)
> 
> +static DEFINE_PER_CPU(struct cpumask, hyperv_tlb_flush_mask);
> +
>  static u64 hyperv_flush_tlb_others_ex(const struct cpumask *cpus,
>  				      const struct flush_tlb_info *info);
> 
> @@ -51,23 +54,23 @@ static inline int fill_gva_list(u64 gva_list[], int offset,
>  	return gva_n - offset;
>  }
> 
> -static bool cpu_is_lazy(int cpu)
> -{
> -	return per_cpu(cpu_tlbstate_shared.is_lazy, cpu);
> -}
> -
>  static void hyperv_flush_tlb_multi(const struct cpumask *cpus,
>  				   const struct flush_tlb_info *info)
>  {
>  	int cpu, vcpu, gva_n, max_gvas;
>  	struct hv_tlb_flush *flush;
> +	struct cpumask *flush_cpus;
>  	u64 status;
>  	unsigned long flags;
> -	bool do_lazy = !info->freed_tables;
> 
>  	trace_hyperv_mmu_flush_tlb_multi(cpus, info);
> 
> -	if (!hv_hypercall_pg)
> +	/*
> +	 * Hypercalls bypass the callbacks that trim mm_cpumask() and complete
> +	 * ASID transitions.
> +	 */
> +	if (!hv_hypercall_pg || info->trim_cpumask ||
> +	    mm_in_asid_transition(info->mm))
>  		goto do_native;
> 
>  	local_irq_save(flags);
> @@ -79,6 +82,30 @@ static void hyperv_flush_tlb_multi(const struct cpumask *cpus,
>  		goto do_native;
>  	}
> 
> +	flush_cpus = this_cpu_ptr(&hyperv_tlb_flush_mask);
> +	if (info->freed_tables) {
> +		cpumask_copy(flush_cpus, cpus);
> +	} else {
> +		cpumask_clear(flush_cpus);
> +		for_each_cpu(cpu, cpus) {
> +			if (cpu_needs_tlb_flush(cpu, info))
> +				__cpumask_set_cpu(cpu, flush_cpus);
> +		}
> +	}
> +
> +	/*
> +	 * An effectively local mm should not keep taking the remote path until
> +	 * the next periodic trim. Let the native callbacks prune stale bits.
> +	 */
> +	if (cpumask_any_but(flush_cpus, smp_processor_id()) >= nr_cpu_ids) {
> +		struct flush_tlb_info local_info = *info;
> +
> +		local_info.trim_cpumask = 1;
> +		local_irq_restore(flags);
> +		native_flush_tlb_multi(cpus, &local_info);
> +		return;
> +	}
> +
>  	if (info->mm) {
>  		/*
>  		 * AddressSpace argument must match the CR3 with PCID bits
> @@ -93,7 +120,7 @@ static void hyperv_flush_tlb_multi(const struct cpumask *cpus,
>  	}
> 
>  	flush->processor_mask = 0;
> -	if (cpumask_equal(cpus, cpu_present_mask)) {
> +	if (cpumask_equal(flush_cpus, cpu_present_mask)) {
>  		flush->flags |= HV_FLUSH_ALL_PROCESSORS;
>  	} else {
>  		/*
> @@ -107,14 +134,12 @@ static void hyperv_flush_tlb_multi(const struct cpumask *cpus,
>  		 * must. We will also check all VP numbers when walking the
>  		 * supplied CPU set to remain correct in all cases.
>  		 */
> -		cpu = cpumask_last(cpus);
> +		cpu = cpumask_last(flush_cpus);
> 
>  		if (cpu < nr_cpumask_bits && hv_cpu_number_to_vp_number(cpu) >= 64)
>  			goto do_ex_hypercall;
> 
> -		for_each_cpu(cpu, cpus) {
> -			if (do_lazy && cpu_is_lazy(cpu))
> -				continue;
> +		for_each_cpu(cpu, flush_cpus) {
>  			vcpu = hv_cpu_number_to_vp_number(cpu);
>  			if (vcpu == VP_INVAL) {
>  				local_irq_restore(flags);
> @@ -127,12 +152,6 @@ static void hyperv_flush_tlb_multi(const struct cpumask *cpus,
>  			__set_bit(vcpu, (unsigned long *)
>  				  &flush->processor_mask);
>  		}
> -
> -		/* nothing to flush if 'processor_mask' ends up being empty */
> -		if (!flush->processor_mask) {
> -			local_irq_restore(flags);
> -			return;
> -		}
>  	}
> 
>  	/*
> @@ -158,7 +177,7 @@ static void hyperv_flush_tlb_multi(const struct cpumask *cpus,
>  	goto check_status;
> 
>  do_ex_hypercall:
> -	status = hyperv_flush_tlb_others_ex(cpus, info);
> +	status = hyperv_flush_tlb_others_ex(flush_cpus, info);
> 
>  check_status:
>  	local_irq_restore(flags);
> @@ -197,9 +216,8 @@ static u64 hyperv_flush_tlb_others_ex(const struct cpumask *cpus,
>  	flush->hv_vp_set.valid_bank_mask = 0;
> 
>  	flush->hv_vp_set.format = HV_GENERIC_SET_SPARSE_4K;
> -	nr_bank = cpumask_to_vpset_skip(&flush->hv_vp_set, cpus,
> -			info->freed_tables ? NULL : cpu_is_lazy);
> -	if (nr_bank < 0)
> +	nr_bank = cpumask_to_vpset(&flush->hv_vp_set, cpus);
> +	if (nr_bank <= 0)
>  		return HV_STATUS_INVALID_PARAMETER;
> 
>  	/*
> diff --git a/arch/x86/include/asm/tlbflush.h b/arch/x86/include/asm/tlbflush.h
> index 70098d448e999..e1e05cc867d5a 100644
> --- a/arch/x86/include/asm/tlbflush.h
> +++ b/arch/x86/include/asm/tlbflush.h
> @@ -264,6 +264,12 @@ void flush_tlb_one_kernel(unsigned long addr);
>  void flush_tlb_multi(const struct cpumask *cpumask,
>  		      const struct flush_tlb_info *info);
> 
> +/*
> + * Do not use for filtering when freeing page tables or during global ASID
> + * transitions.
> + */
> +bool cpu_needs_tlb_flush(int cpu, const struct flush_tlb_info *info);
> +
>  static inline bool is_dyn_asid(u16 asid)
>  {
>  	return asid < TLB_NR_DYN_ASIDS;
> diff --git a/arch/x86/mm/tlb.c b/arch/x86/mm/tlb.c
> index 0c4da320831cb..f2f40928d56a4 100644
> --- a/arch/x86/mm/tlb.c
> +++ b/arch/x86/mm/tlb.c
> @@ -1273,10 +1273,9 @@ static void flush_tlb_func(void *info)
>  			nr_invalidate);
>  }
> 
> -static bool should_flush_tlb(int cpu, void *data)
> +bool cpu_needs_tlb_flush(int cpu, const struct flush_tlb_info *info)
>  {
>  	struct mm_struct *loaded_mm = per_cpu(cpu_tlbstate.loaded_mm, cpu);
> -	struct flush_tlb_info *info = data;
> 
>  	/*
>  	 * Order the 'loaded_mm' and 'is_lazy' against their
> @@ -1311,6 +1310,11 @@ static bool should_flush_tlb(int cpu, void *data)
>  	return false;
>  }
> 
> +static bool should_flush_tlb(int cpu, void *data)
> +{
> +	return cpu_needs_tlb_flush(cpu, data);
> +}
> +
>  static bool should_trim_cpumask(struct mm_struct *mm)
>  {
>  	if (time_after(jiffies, READ_ONCE(mm->context.next_trim_cpumask))) {
> --
> 2.43.0


^ permalink raw reply	[flat|nested] 4+ messages in thread

* Re: [PATCH] x86/hyperv: Account for lazy mm_cpumask maintenance
  2026-10-30  2:39 ` Michael Kelley
@ 2026-10-30  4:00   ` Naman Jain
  2026-10-30  4:09     ` Michael Kelley
  0 siblings, 1 reply; 4+ messages in thread
From: Naman Jain @ 2026-10-30  4:00 UTC (permalink / raw)
  To: Michael Kelley, haiyangz@microsoft.com, wei.liu@kernel.org,
	decui@microsoft.com, tglx@kernel.org, mingo@redhat.com,
	bp@alien8.de, dave.hansen@linux.intel.com, x86@kernel.org,
	luto@kernel.org, peterz@infradead.org
  Cc: hpa@zytor.com, riel@surriel.com, linux-hyperv@vger.kernel.org,
	linux-kernel@vger.kernel.org



On 10/30/2026 8:09 AM, Michael Kelley wrote:
> From: Naman Jain <namjain@linux.microsoft.com> Sent: Wednesday, October 28, 2026 11:43 PM
>>
>> CPUs no longer running an mm may remain in mm_cpumask(). Hyper-V treats
>> these CPUs as flush targets and bypasses the native callbacks that trim
>> the mask. A single-threaded process can therefore keep issuing
>> all-processor flushes after visiting all CPUs and returning to one CPU.
>>
>> Share the native target predicate and filter the mask before building
>> either hypercall format or selecting HV_FLUSH_ALL_PROCESSORS. Preserve
>> lazy-TLB exclusion and unfiltered page-table-freeing flushes, and use
>> native callbacks for periodic trimming and ASID transitions.
>>
>> When no remote targets remain, request immediate trimming through native
>> callbacks so stale memberships do not unnecessarily prevent later
>> invalidations from using the local fast path. Leave lazy CPU-mask
>> maintenance during context switches unchanged.
>>
>> Fixes: 209954cbc7d0 ("x86/mm/tlb: Update mm_cpumask lazily")
>> Signed-off-by: Naman Jain <namjain@linux.microsoft.com>
> 
> I've read the commit message but haven't yet delved into the code for
> this patch. But the commit message feels like it is missing some context,
> particularly with the "Fixes:" tag.
> 
> I recall the last significant change to hyperv_flush_tlb_multi(), which takes
> CPUs in lazy tlb mode into account, and thereby avoids unnecessary flushing
> (commit 493cc07385cf, made in March 2023). It was a case where the
> native x86 flush code had gotten smarter, and the Hyper-V PV flush
> functions had not been updated to be smarter in the same way. Against
> that context, what is the situation here? Commit 209954cbc7d0 came a
> year and a half later in November 2024. Did it make things worse for the
> Hyper-V PV flush functions, and hence is deemed to have "broken"
> them? Or is it another native flush improvement that the Hyper-V PV
> flush functions just don't take advantage of?
> 
> I'm also curious how the issue was noticed. Was there a real-world
> performance problem, or is it a case of looking at the code and
> noticing a native improvement that hadn't been applied to the
> Hyper-V PV flush functions? The scenario of a single-threaded process
> visiting all CPUs and then returning to one CPU seems contrived,
> particularly as CPU counts get large. It may illustrate the improvement
> that is possible, but to me seems unlikely to be how the problem was
> originally noticed.
> 
> I intended to review the code carefully, but the additional context
> would be helpful in doing that review.
> 
> Michael
> 

Thanks Michael for looking into this.

We first observed a performance regression between Linux 6.8 and 7.0
on Azure MI300X VMs running SuperBench's eight-GPU PyTorch LSTM training
benchmark, using distributed data parallelism in FP32 and FP16.
Recreating DataLoader workers each epoch triggered long host-to-GPU
copy stalls and timeouts. We bisected the regression to commit being
fixed and then developed the reduced reproducer to isolate the excessive
Hyper-V TLB flushes. The CPU-visiting test came out of that
investigation, it was not how the problem was originally noticed.

209954cbc7d0 ("x86/mm/tlb: Update mm_cpumask lazily") changed where
stale mm_cpumask entries are removed. Previously, switching a CPU from
mm A to mm B cleared its bit in A's mask. After that change, the bit is
left set for removal by the native TLB flush callback when it finds a
different mm loaded.

The Hyper-V hypercall path bypasses that callback, so historical
memberships can persist. The lazy-TLB check from 493cc07385cf does not
cover this case: a CPU actively running B can have is_lazy == false
while still appearing in A's mask. Hyper-V then unnecessarily targets
it when flushing A. If the historical memberships cover all present
CPUs, the all-processors shortcut is selected before the per-CPU
lazy-TLB checks.

So the later change did make the Hyper-V path worse for affected
workloads, rather than merely adding a native optimization that
Hyper-V could also benefit from. That is the reason for the Fixes tag.
This is a performance regression, not a stale-translation correctness
failure.

The reporter has also tested the patch on a 96-vCPU MI300X VM. The
unpatched SuperBench workload hit the 600-second timeout; two patched
runs completed in 424 and 436 seconds, with all eight ranks validated.
The kernel configurations matched apart from kernel identity, and
the GPU driver source and executable text were unchanged.

For the reduced reproducer, the 96-CPU-history case dropped from
109.196 us per iteration to 1.594 and 1.616 us in the two patched runs,
about a 98.5% reduction. Each iteration contains two mprotect() calls
and a write, these are not individual hypercall timings.

The patch preserves your lazy-TLB exclusion and lazy CPU-mask
maintenance during context switches. It filters the targets before
selecting the hypercall format or the all-processors shortcut, and
uses native callbacks for mask trimming.

I restricted the commit msg to the mentioned 32 vCPU VM because that was
the one I got my hands on. I'll try to update the commit message or the
comments to explain the real workload, bisection, and distinction
between lazy TLB mode and lazy CPU-mask maintenance. But I'll wait for
any review comments to address them together.

Thanks,
Naman

>> ---
>> On a 32-vCPU Hyper-V guest, after a single-threaded process visited all
>> CPUs and returned to one CPU, the reproducer's median time per iteration
>> (two mprotect() calls and a write) dropped from 17.235 us to 1.622 us,
>> a 90.6% reduction. This could be more on higher vCPU VMs.
>>
<snip>


^ permalink raw reply	[flat|nested] 4+ messages in thread

* RE: [PATCH] x86/hyperv: Account for lazy mm_cpumask maintenance
  2026-10-30  4:00   ` Naman Jain
@ 2026-10-30  4:09     ` Michael Kelley
  0 siblings, 0 replies; 4+ messages in thread
From: Michael Kelley @ 2026-10-30  4:09 UTC (permalink / raw)
  To: Naman Jain, Michael Kelley, haiyangz@microsoft.com,
	wei.liu@kernel.org, decui@microsoft.com, tglx@kernel.org,
	mingo@redhat.com, bp@alien8.de, dave.hansen@linux.intel.com,
	x86@kernel.org, luto@kernel.org, peterz@infradead.org
  Cc: hpa@zytor.com, riel@surriel.com, linux-hyperv@vger.kernel.org,
	linux-kernel@vger.kernel.org

From: Naman Jain <namjain@linux.microsoft.com> Sent: Thursday, October 29, 2026 9:01 PM
> 
> On 10/30/2026 8:09 AM, Michael Kelley wrote:
> > From: Naman Jain <namjain@linux.microsoft.com> Sent: Wednesday, October 28, 2026 11:43 PM
> >>
> >> CPUs no longer running an mm may remain in mm_cpumask(). Hyper-V treats
> >> these CPUs as flush targets and bypasses the native callbacks that trim
> >> the mask. A single-threaded process can therefore keep issuing
> >> all-processor flushes after visiting all CPUs and returning to one CPU.
> >>
> >> Share the native target predicate and filter the mask before building
> >> either hypercall format or selecting HV_FLUSH_ALL_PROCESSORS. Preserve
> >> lazy-TLB exclusion and unfiltered page-table-freeing flushes, and use
> >> native callbacks for periodic trimming and ASID transitions.
> >>
> >> When no remote targets remain, request immediate trimming through native
> >> callbacks so stale memberships do not unnecessarily prevent later
> >> invalidations from using the local fast path. Leave lazy CPU-mask
> >> maintenance during context switches unchanged.
> >>
> >> Fixes: 209954cbc7d0 ("x86/mm/tlb: Update mm_cpumask lazily")
> >> Signed-off-by: Naman Jain <namjain@linux.microsoft.com>
> >
> > I've read the commit message but haven't yet delved into the code for
> > this patch. But the commit message feels like it is missing some context,
> > particularly with the "Fixes:" tag.
> >
> > I recall the last significant change to hyperv_flush_tlb_multi(), which takes
> > CPUs in lazy tlb mode into account, and thereby avoids unnecessary flushing
> > (commit 493cc07385cf, made in March 2023). It was a case where the
> > native x86 flush code had gotten smarter, and the Hyper-V PV flush
> > functions had not been updated to be smarter in the same way. Against
> > that context, what is the situation here? Commit 209954cbc7d0 came a
> > year and a half later in November 2024. Did it make things worse for the
> > Hyper-V PV flush functions, and hence is deemed to have "broken"
> > them? Or is it another native flush improvement that the Hyper-V PV
> > flush functions just don't take advantage of?
> >
> > I'm also curious how the issue was noticed. Was there a real-world
> > performance problem, or is it a case of looking at the code and
> > noticing a native improvement that hadn't been applied to the
> > Hyper-V PV flush functions? The scenario of a single-threaded process
> > visiting all CPUs and then returning to one CPU seems contrived,
> > particularly as CPU counts get large. It may illustrate the improvement
> > that is possible, but to me seems unlikely to be how the problem was
> > originally noticed.
> >
> > I intended to review the code carefully, but the additional context
> > would be helpful in doing that review.
> >
> > Michael
> >
> 
> Thanks Michael for looking into this.
> 
> We first observed a performance regression between Linux 6.8 and 7.0
> on Azure MI300X VMs running SuperBench's eight-GPU PyTorch LSTM training
> benchmark, using distributed data parallelism in FP32 and FP16.
> Recreating DataLoader workers each epoch triggered long host-to-GPU
> copy stalls and timeouts. We bisected the regression to commit being
> fixed and then developed the reduced reproducer to isolate the excessive
> Hyper-V TLB flushes. The CPU-visiting test came out of that
> investigation, it was not how the problem was originally noticed.
> 
> 209954cbc7d0 ("x86/mm/tlb: Update mm_cpumask lazily") changed where
> stale mm_cpumask entries are removed. Previously, switching a CPU from
> mm A to mm B cleared its bit in A's mask. After that change, the bit is
> left set for removal by the native TLB flush callback when it finds a
> different mm loaded.
> 
> The Hyper-V hypercall path bypasses that callback, so historical
> memberships can persist. The lazy-TLB check from 493cc07385cf does not
> cover this case: a CPU actively running B can have is_lazy == false
> while still appearing in A's mask. Hyper-V then unnecessarily targets
> it when flushing A. If the historical memberships cover all present
> CPUs, the all-processors shortcut is selected before the per-CPU
> lazy-TLB checks.
> 
> So the later change did make the Hyper-V path worse for affected
> workloads, rather than merely adding a native optimization that
> Hyper-V could also benefit from. That is the reason for the Fixes tag.
> This is a performance regression, not a stale-translation correctness
> failure.

Thanks. That's exactly what I wanted to know as background
information. It will help a lot when reviewing your patch.

> 
> The reporter has also tested the patch on a 96-vCPU MI300X VM. The
> unpatched SuperBench workload hit the 600-second timeout; two patched
> runs completed in 424 and 436 seconds, with all eight ranks validated.
> The kernel configurations matched apart from kernel identity, and
> the GPU driver source and executable text were unchanged.

Good info. That's a pretty significant difference in a macro-level
benchmarking tool.

Michael

> 
> For the reduced reproducer, the 96-CPU-history case dropped from
> 109.196 us per iteration to 1.594 and 1.616 us in the two patched runs,
> about a 98.5% reduction. Each iteration contains two mprotect() calls
> and a write, these are not individual hypercall timings.
> 
> The patch preserves your lazy-TLB exclusion and lazy CPU-mask
> maintenance during context switches. It filters the targets before
> selecting the hypercall format or the all-processors shortcut, and
> uses native callbacks for mask trimming.
> 
> I restricted the commit msg to the mentioned 32 vCPU VM because that was
> the one I got my hands on. I'll try to update the commit message or the
> comments to explain the real workload, bisection, and distinction
> between lazy TLB mode and lazy CPU-mask maintenance. But I'll wait for
> any review comments to address them together.
> 
> Thanks,
> Naman
> 
> >> ---
> >> On a 32-vCPU Hyper-V guest, after a single-threaded process visited all
> >> CPUs and returned to one CPU, the reproducer's median time per iteration
> >> (two mprotect() calls and a write) dropped from 17.235 us to 1.622 us,
> >> a 90.6% reduction. This could be more on higher vCPU VMs.
> >>
> <snip>
> 


^ permalink raw reply	[flat|nested] 4+ messages in thread

end of thread, other threads:[~2026-10-30  4:10 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-29  6:42 [PATCH] x86/hyperv: Account for lazy mm_cpumask maintenance Naman Jain
2026-10-30  2:39 ` Michael Kelley
2026-10-30  4:00   ` Naman Jain
2026-10-30  4:09     ` Michael Kelley

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox