Linux-mm Archive on lore.kernel.org
 help / color / mirror / Atom feed
From: "Huang, Ying" <ying.huang@linux.alibaba.com>
To: Zhiteng Qiu <qiuzhiteng@hygon.cn>
Cc: Andrew Morton <akpm@linux-foundation.org>,
	 David Hildenbrand <david@kernel.org>,  Zi Yan <ziy@nvidia.com>,
	 Matthew Brost <matthew.brost@intel.com>,
	 Joshua Hahn <joshua.hahnjy@gmail.com>,
	Byungchul Park <byungchul@sk.com>,
	 Gregory Price <gourry@gourry.net>,
	Alistair Popple <apopple@nvidia.com>,
	 Xin Hao <xhao@linux.alibaba.com>,
	Dave Hansen <dave.hansen@linux.intel.com>,  <linux-mm@kvack.org>,
	<linux-kernel@vger.kernel.org>
Subject: Re: [PATCH v2] mm/migrate: don't batch the TLB flush when there is nothing to batch
Date: Sun, 08 Nov 2026 20:52:07 +0800	[thread overview]
Message-ID: <878q33wknc.fsf@DESKTOP-5N7EMDA> (raw)
In-Reply-To: <20261107143456.1590658-1-qiuzhiteng@hygon.cn> (Zhiteng Qiu's message of "Sat, 7 Nov 2026 22:34:55 +0800")

Zhiteng Qiu <qiuzhiteng@hygon.cn> writes:

> Since commit 7e12beb8ca2a ("migrate_pages: batch flushing TLB"), every
> MIGRATE_ASYNC migration unmaps with TTU_BATCH_FLUSH.  That includes NUMA
> balancing and memory-tiering promotion, which migrate one folio per call
> through migrate_misplaced_folio().  For such a call there is nothing to
> batch, but the batched path still has two side effects:
>
>  - Each batched unmap bumps mm->tlb_flush_batched, and only
>    flush_tlb_batched_pending() marks it flushed.  While NUMA balancing
>    keeps migrating, the scanner's change_pte_range() therefore does a
>    flush_tlb_mm() for every PTE table it changes, which makes the scan
>    much slower.
>
>  - The PTE being migrated is PROT_NONE, which normally needs no flush
>    (b99a342d4f11).  But pte_accessible() treats PROT_NONE as accessible
>    while mm->tlb_flush_pending is raised (on x86, arm64 and riscv),
>    and it is raised for the whole, now much longer, scan.  On x86,
>    each migration that overlaps a scan then does a full
>    arch_tlbbatch_flush() to every CPU in mm_cpumask().
>
> One pass on an unpatched kernel:
>
>   CPU A (task_numa_work)          CPU B..N (workload threads)
>   ----------------------          ---------------------------
>   tlb_gather_mmu()
>     tlb_flush_pending++
>   change_pte_range(table 1)
>     PTEs -> PROT_NONE
>                                   access page in table 1
>                                   do_numa_page()
>                                    migrate_misplaced_folio()
>                                     try_to_migrate_one()
>                                      PROT_NONE, but pending set:
>                                      pte_accessible() == true
>                                      tlb_flush_batched++
>                                     try_to_unmap_flush()
>                                      full flush, IPI to ~380 CPUs
>   change_pte_range(table 2)
>     flush_tlb_batched_pending()
>       pending != flushed:
>       flush_tlb_mm(), IPI to ~380 CPUs
>     PTEs -> PROT_NONE
>                                   (same for pages in table 2)
>   ... 128 PTE tables, ~0.7 ms each ...
>   tlb_finish_mmu()                    pass took ~90 ms, not ~7 ms
>     tlb_flush_pending--
>
> The scan period is long, but the passes are not independent of the
> faults: passes cover only 9-12% of wall time, yet 33-42% of the
> hint-fault migrations overlap one (two separate runs).  60% of those
> are for pages in the chunk being scanned, nearly all the rest for
> pages scanned one or two passes earlier.  The scan creates the faults
> that then overlap it.
>
> On a 2-socket, 384-CPU x86 server without INVLPGB, a 4 KiB-page,
> memory-heavy multithreaded workload takes about 200 TLB shootdown
> interrupts per page migrated by NUMA balancing, and balancing costs it
> 13-16% of its throughput.  With bpftrace on that workload, the scanner
> did about 11,600 full flushes in 55 s, its average mmu_gather took
> about 90 ms (6.7 ms with this patch), and 42% of the migrations saw
> tlb_flush_pending set (3.5% with this patch).  All NUMA balancing
> tunables were at their defaults (scan_delay_ms 1000,
> scan_period_min_ms 1000, scan_period_max_ms 60000, scan_size_mb 256).
>
> Skip the batched flush only when the batch cannot merge anything: the
> folio is the only one in this migrate_pages_batch() call, it is not a
> large folio, and it is mapped only once.  The rmap walk then meets at
> most one PTE, and ptep_clear_flush() invalidates just that page
> without touching mm->tlb_flush_batched.  Everything else keeps the
> batched flush, including PTE-mapped large folios, folios mapped more
> than once, and the batch of sub-folios produced when a partially
> mapped large folio is split during migration.
>
> PMD-mapped THPs already invalidate their mappings through
> set_pmd_migration_entry(), independently of TTU_BATCH_FLUSH, so their
> flush behaviour is unchanged.  arm64 has the same pte_accessible()
> rule, but its flushes are broadcast TLBIs rather than IPIs; the effect
> there has not been measured.
>
> migrate_folio_unmap() cannot see how many folios its caller is
> migrating, so migrate_pages_batch() passes that down.
>
> Measured on v7.3: 120 GiB anonymous working set, 384 threads, 180 s per
> run, three A/B pairs (numa_balancing on vs. off, same boot) per kernel.
>
>                                     v7.3            patched
>   throughput loss from balancing   13.0-16.5%      0.5-6.3%
>   kernel cycles per hinting PTE    1.18-1.22M      0.28-0.31M
>   TLB interrupts / migrated page   202-210         12.7-13.6

IMHO, the description above is a bit too long.  Could you try to make it
more concise?

> Signed-off-by: Zhiteng Qiu <qiuzhiteng@hygon.cn>
> ---
> Changes in v2:
>  - Pass the try_to_migrate() flags directly and use single_folio in
>    the VM_WARN_ON_ONCE(), as suggested by Ying.  No functional change.
>  - Rewrote the changelog after measuring why b99a342d4f11 does not
>    avoid the flush here (see the discussion on v1:
>    https://lore.kernel.org/r/20261106041628.1177963-1-qiuzhiteng@hygon.cn).
>
> Notes:
>
> An earlier version decided on reason == MR_NUMA_MISPLACED.  That also
> dropped batching for the split sub-folio batch mentioned above.  In a
> test that splits partially mapped 2 MiB THPs during NUMA migration, the
> earlier version raised TLB interrupts per migrated page from 0.13 to
> 16.1; this version keeps them at 0.11.  For 64 KiB mTHP and for a
> MAP_SHARED mapping used by two processes, this version behaves like the
> base kernel (TLB interrupts per migrated page: mTHP 2.2-2.7 patched vs.
> 2.6-3.3 base; shared 31.3-31.5 vs. 31.2-31.7).
>
> The earlier version behaves the same as this one for order-0, private,
> single-mapped memory.  These results were measured with it:
>  - 192 CPUs (SMT off, 192 threads, 4 pairs): balancing loss
>    3.1-14.7% patched vs. 7.4-54.3% base.
>  - Memory first-touched on the remote socket, threads on the local
>    socket (6 runs per kernel): no regression in time to convergence or
>    in throughput after convergence.
>  - 2 MiB THP (2 pairs): no difference, as expected for PMD-mapped
>    folios.
>  - A 30-minute run with compaction forced every 30 s: no warnings.
>
> With this version, tools/testing/selftests/mm/migration passes 6/6
> (including the THP and hugetlb cases), the LTP move_pages and
> migrate_pages cases pass 14/14, and the scenario tests above saw no data
> mismatches.
>
> There is no Fixes: tag.  This changes a performance trade-off in code
> that has been there since v6.4, and nobody has reported it as a
> regression.  Please add one if you see it that way.
>
>  mm/migrate.c | 22 ++++++++++++++++++----
>  1 file changed, 18 insertions(+), 4 deletions(-)
>
> diff --git a/mm/migrate.c b/mm/migrate.c
> index cb699ea59d33..c2131169317d 100644
> --- a/mm/migrate.c
> +++ b/mm/migrate.c
> @@ -1222,7 +1222,7 @@ static void migrate_folio_done(struct folio *src,
>  static int migrate_folio_unmap(new_folio_t get_new_folio,
>  		free_folio_t put_new_folio, unsigned long private,
>  		struct folio *src, struct folio **dstp, enum migrate_mode mode,
> -		struct list_head *ret)
> +		bool single_folio, struct list_head *ret)
>  {
>  	struct folio *dst;
>  	int rc = -EAGAIN;
> @@ -1344,7 +1344,19 @@ static int migrate_folio_unmap(new_folio_t get_new_folio,
>  		/* Establish migration ptes */
>  		VM_BUG_ON_FOLIO(folio_test_anon(src) &&
>  			       !folio_test_ksm(src) && !anon_vma, src);
> -		try_to_migrate(src, mode == MIGRATE_ASYNC ? TTU_BATCH_FLUSH : 0);
> +		enum ttu_flags ttu = mode == MIGRATE_ASYNC ? TTU_BATCH_FLUSH : 0;
> +
> +		/*
> +		 * Batching only pays off when the rmap walk can meet more than
> +		 * one PTE.  A lone order-0 folio with a single mapping has
> +		 * exactly one: batching it gains nothing, and it leaves
> +		 * mm->tlb_flush_batched pending, so later page table changes
> +		 * in this mm (e.g. NUMA scanning) flush the whole mm.
> +		 */
> +		if (single_folio && !folio_test_large(src) &&
> +		    !folio_maybe_mapped_shared(src))
> +			ttu &= ~TTU_BATCH_FLUSH;

Is it possible to use a simplified version as below?

        enum ttu_flags ttu = 0;

        if (mode == MIGRATE_ASYNC &&
            (!single_folio || folio_mapcount(src) > 1))
                ttu = TTU_BATCH_FLUSH;

> +		try_to_migrate(src, ttu);
>  		old_folio_state |= FOLIO_WAS_MAPPED;
>  	}
>  
> @@ -1835,9 +1847,10 @@ static int migrate_pages_batch(struct list_head *from,
>  	LIST_HEAD(unmap_folios);
>  	LIST_HEAD(dst_folios);
>  	bool nosplit = (reason == MR_NUMA_MISPLACED);
> +	bool single_folio = list_is_singular(from);
>  
>  	VM_WARN_ON_ONCE(mode != MIGRATE_ASYNC &&
> -			!list_empty(from) && !list_is_singular(from));
> +			!list_empty(from) && !single_folio);
>  
>  	for (pass = 0; pass < nr_pass && retry; pass++) {
>  		retry = 0;
> @@ -1925,7 +1938,8 @@ static int migrate_pages_batch(struct list_head *from,
>  			}
>  
>  			rc = migrate_folio_unmap(get_new_folio, put_new_folio,
> -					private, folio, &dst, mode, ret_folios);
> +					private, folio, &dst, mode, single_folio,
> +					ret_folios);
>  			/*
>  			 * The rules are:
>  			 *	0: folio will be put on unmap_folios list,
>
> base-commit: 56b90edee105e976d4870b9a048e6965dc0eac56

---
Best Regards,
Huang, Ying


  reply	other threads:[~2026-11-08 12:52 UTC|newest]

Thread overview: 11+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-06  4:16 [PATCH] mm/migrate: don't batch the TLB flush when there is nothing to batch Zhiteng Qiu
2026-11-06  9:37 ` Huang, Ying
2026-11-06 10:09   ` Zhiteng Qiu
2026-11-06 17:50     ` Zhiteng Qiu
2026-11-07  6:54       ` Huang, Ying
2026-11-07 11:07         ` Zhiteng Qiu
2026-11-07 12:22           ` Huang, Ying
2026-11-07 12:26 ` Huang, Ying
2026-11-07 14:34 ` [PATCH v2] " Zhiteng Qiu
2026-11-08 12:52   ` Huang, Ying [this message]
2026-11-08 14:42     ` Zi Yan

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=878q33wknc.fsf@DESKTOP-5N7EMDA \
    --to=ying.huang@linux.alibaba.com \
    --cc=akpm@linux-foundation.org \
    --cc=apopple@nvidia.com \
    --cc=byungchul@sk.com \
    --cc=dave.hansen@linux.intel.com \
    --cc=david@kernel.org \
    --cc=gourry@gourry.net \
    --cc=joshua.hahnjy@gmail.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-mm@kvack.org \
    --cc=matthew.brost@intel.com \
    --cc=qiuzhiteng@hygon.cn \
    --cc=xhao@linux.alibaba.com \
    --cc=ziy@nvidia.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox