mbox series

[0/6] mm: split underutilized THPs

Message ID 20240730125346.1580150-1-usamaarif642@gmail.com (mailing list archive)
Headers show
Series mm: split underutilized THPs | expand

Message

Usama Arif July 30, 2024, 12:45 p.m. UTC
The current upstream default policy for THP is always. However, Meta
uses madvise in production as the current THP=always policy vastly
overprovisions THPs in sparsely accessed memory areas, resulting in
excessive memory pressure and premature OOM killing.
Using madvise + relying on khugepaged has certain drawbacks over
THP=always. Using madvise hints mean THPs aren't "transparent" and
require userspace changes. Waiting for khugepaged to scan memory and
collapse pages into THP can be slow and unpredictable in terms of performance
(i.e. you dont know when the collapse will happen), while production
environments require predictable performance. If there is enough memory
available, its better for both performance and predictability to have
a THP from fault time, i.e. THP=always rather than wait for khugepaged
to collapse it, and deal with sparsely populated THPs when the system is
running out of memory.

This patch-series is an attempt to mitigate the issue of running out of
memory when THP is always enabled. During runtime whenever a THP is being
faulted in or collapsed by khugepaged, the THP is added to a list.
Whenever memory reclaim happens, the kernel runs the deferred_split
shrinker which goes through the list and checks if the THP was underutilized,
i.e. how many of the base 4K pages of the entire THP were zero-filled.
If this number goes above a certain threshold, the shrinker will attempt
to split that THP. Then at remap time, the pages that were zero-filled are
not remapped, hence saving memory. This method avoids the downside of
wasting memory in areas where THP is sparsely filled when THP is always
enabled, while still providing the upside THPs like reduced TLB misses without
having to use madvise.

Meta production workloads that were CPU bound (>99% CPU utilzation) were
tested with THP shrinker. The results after 2 hours are as follows:

                            | THP=madvise |  THP=always   | THP=always
                            |             |               | + shrinker series
                            |             |               | + max_ptes_none=409
-----------------------------------------------------------------------------
Performance improvement     |      -      |    +1.8%      |     +1.7%
(over THP=madvise)          |             |               |
-----------------------------------------------------------------------------
Memory usage                |    54.6G    | 58.8G (+7.7%) |   55.9G (+2.4%)
-----------------------------------------------------------------------------
max_ptes_none=409 means that any THP that has more than 409 out of 512
(80%) zero filled filled pages will be split.

To test out the patches, the below commands without the shrinker will
invoke OOM killer immediately and kill stress, but will not fail with
the shrinker:

echo 450 > /sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none
mkdir /sys/fs/cgroup/test
echo $$ > /sys/fs/cgroup/test/cgroup.procs
echo 20M > /sys/fs/cgroup/test/memory.max
echo 0 > /sys/fs/cgroup/test/memory.swap.max
# allocate twice memory.max for each stress worker and touch 40/512 of
# each THP, i.e. vm-stride 50K.
# With the shrinker, max_ptes_none of 470 and below won't invoke OOM
# killer.
# Without the shrinker, OOM killer is invoked immediately irrespective
# of max_ptes_none value and kill stress.
stress --vm 1 --vm-bytes 40M --vm-stride 50K

Patches 1-2 add back helper functions that were previously removed
to operate on page lists (needed by patch 3).
Patch 3 is an optimization to free zapped tail pages rather than
waiting for page reclaim or migration.
Patch 4 is a prerequisite for THP shrinker to not remap zero-filled
subpages when splitting THP.
Patches 6 adds support for THP shrinker.

(This patch-series restarts the work on having a THP shrinker in kernel
originally done in
https://lore.kernel.org/all/cover.1667454613.git.alexlzhu@fb.com/.
The THP shrinker in this series is significantly different than the
original one, hence its labelled v1 (although the prerequisite to not
remap clean subpages is the same).)

Alexander Zhu (1):
  mm: add selftests to split_huge_page() to verify unmap/zap of zero
    pages

Usama Arif (3):
  Revert "memcg: remove mem_cgroup_uncharge_list()"
  Revert "mm: remove free_unref_page_list()"
  mm: split underutilized THPs

Yu Zhao (2):
  mm: free zapped tail pages when splitting isolated thp
  mm: don't remap unused subpages when splitting isolated thp

 Documentation/admin-guide/mm/transhuge.rst    |   6 +
 include/linux/huge_mm.h                       |   4 +-
 include/linux/khugepaged.h                    |   1 +
 include/linux/memcontrol.h                    |  12 ++
 include/linux/mm_types.h                      |   2 +
 include/linux/rmap.h                          |   2 +-
 include/linux/vm_event_item.h                 |   1 +
 mm/huge_memory.c                              | 152 +++++++++++++++---
 mm/hugetlb.c                                  |   1 +
 mm/internal.h                                 |   5 +-
 mm/khugepaged.c                               |   3 +-
 mm/memcontrol.c                               |  22 ++-
 mm/migrate.c                                  |  76 +++++++--
 mm/migrate_device.c                           |   4 +-
 mm/page_alloc.c                               |  18 +++
 mm/rmap.c                                     |   2 +-
 mm/vmscan.c                                   |   3 +-
 mm/vmstat.c                                   |   1 +
 .../selftests/mm/split_huge_page_test.c       | 113 +++++++++++++
 tools/testing/selftests/mm/vm_util.c          |  22 +++
 tools/testing/selftests/mm/vm_util.h          |   1 +
 21 files changed, 414 insertions(+), 37 deletions(-)

Comments

David Hildenbrand July 30, 2024, 2:35 p.m. UTC | #1
On 30.07.24 14:45, Usama Arif wrote:
> The current upstream default policy for THP is always. However, Meta
> uses madvise in production as the current THP=always policy vastly
> overprovisions THPs in sparsely accessed memory areas, resulting in
> excessive memory pressure and premature OOM killing.
> Using madvise + relying on khugepaged has certain drawbacks over
> THP=always. Using madvise hints mean THPs aren't "transparent" and
> require userspace changes. Waiting for khugepaged to scan memory and
> collapse pages into THP can be slow and unpredictable in terms of performance
> (i.e. you dont know when the collapse will happen), while production
> environments require predictable performance. If there is enough memory
> available, its better for both performance and predictability to have
> a THP from fault time, i.e. THP=always rather than wait for khugepaged
> to collapse it, and deal with sparsely populated THPs when the system is
> running out of memory.
> 
> This patch-series is an attempt to mitigate the issue of running out of
> memory when THP is always enabled. During runtime whenever a THP is being
> faulted in or collapsed by khugepaged, the THP is added to a list.
> Whenever memory reclaim happens, the kernel runs the deferred_split
> shrinker which goes through the list and checks if the THP was underutilized,
> i.e. how many of the base 4K pages of the entire THP were zero-filled.
> If this number goes above a certain threshold, the shrinker will attempt
> to split that THP. Then at remap time, the pages that were zero-filled are
> not remapped, hence saving memory. This method avoids the downside of
> wasting memory in areas where THP is sparsely filled when THP is always
> enabled, while still providing the upside THPs like reduced TLB misses without
> having to use madvise.
> 
> Meta production workloads that were CPU bound (>99% CPU utilzation) were
> tested with THP shrinker. The results after 2 hours are as follows:
> 
>                              | THP=madvise |  THP=always   | THP=always
>                              |             |               | + shrinker series
>                              |             |               | + max_ptes_none=409
> -----------------------------------------------------------------------------
> Performance improvement     |      -      |    +1.8%      |     +1.7%
> (over THP=madvise)          |             |               |
> -----------------------------------------------------------------------------
> Memory usage                |    54.6G    | 58.8G (+7.7%) |   55.9G (+2.4%)
> -----------------------------------------------------------------------------
> max_ptes_none=409 means that any THP that has more than 409 out of 512
> (80%) zero filled filled pages will be split.
> 
> To test out the patches, the below commands without the shrinker will
> invoke OOM killer immediately and kill stress, but will not fail with
> the shrinker:
> 
> echo 450 > /sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none
> mkdir /sys/fs/cgroup/test
> echo $$ > /sys/fs/cgroup/test/cgroup.procs
> echo 20M > /sys/fs/cgroup/test/memory.max
> echo 0 > /sys/fs/cgroup/test/memory.swap.max
> # allocate twice memory.max for each stress worker and touch 40/512 of
> # each THP, i.e. vm-stride 50K.
> # With the shrinker, max_ptes_none of 470 and below won't invoke OOM
> # killer.
> # Without the shrinker, OOM killer is invoked immediately irrespective
> # of max_ptes_none value and kill stress.
> stress --vm 1 --vm-bytes 40M --vm-stride 50K
> 
> Patches 1-2 add back helper functions that were previously removed
> to operate on page lists (needed by patch 3).
> Patch 3 is an optimization to free zapped tail pages rather than
> waiting for page reclaim or migration.
> Patch 4 is a prerequisite for THP shrinker to not remap zero-filled
> subpages when splitting THP.
> Patches 6 adds support for THP shrinker.
> 
> (This patch-series restarts the work on having a THP shrinker in kernel
> originally done in
> https://lore.kernel.org/all/cover.1667454613.git.alexlzhu@fb.com/.
> The THP shrinker in this series is significantly different than the
> original one, hence its labelled v1 (although the prerequisite to not
> remap clean subpages is the same).)

As shared previously, there is one issue with uffd (even when currently 
not active for a VMA!), where we must not zap present page table entries.

Something that is always possible (assuming no GUP pins of course, 
which) is replacing the zero-filled subpages by shared zeropages.

Is that being done in this patch set already, or are we creating 
pte_none() entries?
Usama Arif July 30, 2024, 3:14 p.m. UTC | #2
On 30/07/2024 15:35, David Hildenbrand wrote:
> On 30.07.24 14:45, Usama Arif wrote:
>> The current upstream default policy for THP is always. However, Meta
>> uses madvise in production as the current THP=always policy vastly
>> overprovisions THPs in sparsely accessed memory areas, resulting in
>> excessive memory pressure and premature OOM killing.
>> Using madvise + relying on khugepaged has certain drawbacks over
>> THP=always. Using madvise hints mean THPs aren't "transparent" and
>> require userspace changes. Waiting for khugepaged to scan memory and
>> collapse pages into THP can be slow and unpredictable in terms of performance
>> (i.e. you dont know when the collapse will happen), while production
>> environments require predictable performance. If there is enough memory
>> available, its better for both performance and predictability to have
>> a THP from fault time, i.e. THP=always rather than wait for khugepaged
>> to collapse it, and deal with sparsely populated THPs when the system is
>> running out of memory.
>>
>> This patch-series is an attempt to mitigate the issue of running out of
>> memory when THP is always enabled. During runtime whenever a THP is being
>> faulted in or collapsed by khugepaged, the THP is added to a list.
>> Whenever memory reclaim happens, the kernel runs the deferred_split
>> shrinker which goes through the list and checks if the THP was underutilized,
>> i.e. how many of the base 4K pages of the entire THP were zero-filled.
>> If this number goes above a certain threshold, the shrinker will attempt
>> to split that THP. Then at remap time, the pages that were zero-filled are
>> not remapped, hence saving memory. This method avoids the downside of
>> wasting memory in areas where THP is sparsely filled when THP is always
>> enabled, while still providing the upside THPs like reduced TLB misses without
>> having to use madvise.
>>
>> Meta production workloads that were CPU bound (>99% CPU utilzation) were
>> tested with THP shrinker. The results after 2 hours are as follows:
>>
>>                              | THP=madvise |  THP=always   | THP=always
>>                              |             |               | + shrinker series
>>                              |             |               | + max_ptes_none=409
>> -----------------------------------------------------------------------------
>> Performance improvement     |      -      |    +1.8%      |     +1.7%
>> (over THP=madvise)          |             |               |
>> -----------------------------------------------------------------------------
>> Memory usage                |    54.6G    | 58.8G (+7.7%) |   55.9G (+2.4%)
>> -----------------------------------------------------------------------------
>> max_ptes_none=409 means that any THP that has more than 409 out of 512
>> (80%) zero filled filled pages will be split.
>>
>> To test out the patches, the below commands without the shrinker will
>> invoke OOM killer immediately and kill stress, but will not fail with
>> the shrinker:
>>
>> echo 450 > /sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none
>> mkdir /sys/fs/cgroup/test
>> echo $$ > /sys/fs/cgroup/test/cgroup.procs
>> echo 20M > /sys/fs/cgroup/test/memory.max
>> echo 0 > /sys/fs/cgroup/test/memory.swap.max
>> # allocate twice memory.max for each stress worker and touch 40/512 of
>> # each THP, i.e. vm-stride 50K.
>> # With the shrinker, max_ptes_none of 470 and below won't invoke OOM
>> # killer.
>> # Without the shrinker, OOM killer is invoked immediately irrespective
>> # of max_ptes_none value and kill stress.
>> stress --vm 1 --vm-bytes 40M --vm-stride 50K
>>
>> Patches 1-2 add back helper functions that were previously removed
>> to operate on page lists (needed by patch 3).
>> Patch 3 is an optimization to free zapped tail pages rather than
>> waiting for page reclaim or migration.
>> Patch 4 is a prerequisite for THP shrinker to not remap zero-filled
>> subpages when splitting THP.
>> Patches 6 adds support for THP shrinker.
>>
>> (This patch-series restarts the work on having a THP shrinker in kernel
>> originally done in
>> https://lore.kernel.org/all/cover.1667454613.git.alexlzhu@fb.com/.
>> The THP shrinker in this series is significantly different than the
>> original one, hence its labelled v1 (although the prerequisite to not
>> remap clean subpages is the same).)
> 
> As shared previously, there is one issue with uffd (even when currently not active for a VMA!), where we must not zap present page table entries.
> 
> Something that is always possible (assuming no GUP pins of course, which) is replacing the zero-filled subpages by shared zeropages.
> 
> Is that being done in this patch set already, or are we creating pte_none() entries?
> 

I think thats done in Patch 4/6. In function try_to_unmap_unused, we have below which I think does what you are suggesting? i.e. point to shared zeropage and not clear pte for uffd armed vma.

	if (userfaultfd_armed(pvmw->vma)) {
		newpte = pte_mkspecial(pfn_pte(page_to_pfn(ZERO_PAGE(pvmw->address)),
					       pvmw->vma->vm_page_prot));
		ptep_clear_flush(pvmw->vma, pvmw->address, pvmw->pte);
		set_pte_at(pvmw->vma->vm_mm, pvmw->address, pvmw->pte, newpte);
	}
Usama Arif July 30, 2024, 3:19 p.m. UTC | #3
On 30/07/2024 16:14, Usama Arif wrote:
> 
> 
> On 30/07/2024 15:35, David Hildenbrand wrote:
>> On 30.07.24 14:45, Usama Arif wrote:
>>> The current upstream default policy for THP is always. However, Meta
>>> uses madvise in production as the current THP=always policy vastly
>>> overprovisions THPs in sparsely accessed memory areas, resulting in
>>> excessive memory pressure and premature OOM killing.
>>> Using madvise + relying on khugepaged has certain drawbacks over
>>> THP=always. Using madvise hints mean THPs aren't "transparent" and
>>> require userspace changes. Waiting for khugepaged to scan memory and
>>> collapse pages into THP can be slow and unpredictable in terms of performance
>>> (i.e. you dont know when the collapse will happen), while production
>>> environments require predictable performance. If there is enough memory
>>> available, its better for both performance and predictability to have
>>> a THP from fault time, i.e. THP=always rather than wait for khugepaged
>>> to collapse it, and deal with sparsely populated THPs when the system is
>>> running out of memory.
>>>
>>> This patch-series is an attempt to mitigate the issue of running out of
>>> memory when THP is always enabled. During runtime whenever a THP is being
>>> faulted in or collapsed by khugepaged, the THP is added to a list.
>>> Whenever memory reclaim happens, the kernel runs the deferred_split
>>> shrinker which goes through the list and checks if the THP was underutilized,
>>> i.e. how many of the base 4K pages of the entire THP were zero-filled.
>>> If this number goes above a certain threshold, the shrinker will attempt
>>> to split that THP. Then at remap time, the pages that were zero-filled are
>>> not remapped, hence saving memory. This method avoids the downside of
>>> wasting memory in areas where THP is sparsely filled when THP is always
>>> enabled, while still providing the upside THPs like reduced TLB misses without
>>> having to use madvise.
>>>
>>> Meta production workloads that were CPU bound (>99% CPU utilzation) were
>>> tested with THP shrinker. The results after 2 hours are as follows:
>>>
>>>                              | THP=madvise |  THP=always   | THP=always
>>>                              |             |               | + shrinker series
>>>                              |             |               | + max_ptes_none=409
>>> -----------------------------------------------------------------------------
>>> Performance improvement     |      -      |    +1.8%      |     +1.7%
>>> (over THP=madvise)          |             |               |
>>> -----------------------------------------------------------------------------
>>> Memory usage                |    54.6G    | 58.8G (+7.7%) |   55.9G (+2.4%)
>>> -----------------------------------------------------------------------------
>>> max_ptes_none=409 means that any THP that has more than 409 out of 512
>>> (80%) zero filled filled pages will be split.
>>>
>>> To test out the patches, the below commands without the shrinker will
>>> invoke OOM killer immediately and kill stress, but will not fail with
>>> the shrinker:
>>>
>>> echo 450 > /sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none
>>> mkdir /sys/fs/cgroup/test
>>> echo $$ > /sys/fs/cgroup/test/cgroup.procs
>>> echo 20M > /sys/fs/cgroup/test/memory.max
>>> echo 0 > /sys/fs/cgroup/test/memory.swap.max
>>> # allocate twice memory.max for each stress worker and touch 40/512 of
>>> # each THP, i.e. vm-stride 50K.
>>> # With the shrinker, max_ptes_none of 470 and below won't invoke OOM
>>> # killer.
>>> # Without the shrinker, OOM killer is invoked immediately irrespective
>>> # of max_ptes_none value and kill stress.
>>> stress --vm 1 --vm-bytes 40M --vm-stride 50K
>>>
>>> Patches 1-2 add back helper functions that were previously removed
>>> to operate on page lists (needed by patch 3).
>>> Patch 3 is an optimization to free zapped tail pages rather than
>>> waiting for page reclaim or migration.
>>> Patch 4 is a prerequisite for THP shrinker to not remap zero-filled
>>> subpages when splitting THP.
>>> Patches 6 adds support for THP shrinker.
>>>
>>> (This patch-series restarts the work on having a THP shrinker in kernel
>>> originally done in
>>> https://lore.kernel.org/all/cover.1667454613.git.alexlzhu@fb.com/.
>>> The THP shrinker in this series is significantly different than the
>>> original one, hence its labelled v1 (although the prerequisite to not
>>> remap clean subpages is the same).)
>>
>> As shared previously, there is one issue with uffd (even when currently not active for a VMA!), where we must not zap present page table entries.
>>
>> Something that is always possible (assuming no GUP pins of course, which) is replacing the zero-filled subpages by shared zeropages.
>>
>> Is that being done in this patch set already, or are we creating pte_none() entries?
>>
> 
> I think thats done in Patch 4/6. In function try_to_unmap_unused, we have below which I think does what you are suggesting? i.e. point to shared zeropage and not clear pte for uffd armed vma.
> 
> 	if (userfaultfd_armed(pvmw->vma)) {
> 		newpte = pte_mkspecial(pfn_pte(page_to_pfn(ZERO_PAGE(pvmw->address)),
> 					       pvmw->vma->vm_page_prot));
> 		ptep_clear_flush(pvmw->vma, pvmw->address, pvmw->pte);
> 		set_pte_at(pvmw->vma->vm_mm, pvmw->address, pvmw->pte, newpte);
> 	}


Ah are you suggesting userfaultfd_armed(pvmw->vma) will evaluate to false even if its uffd? I think something like below would work in that case.

diff --git a/mm/migrate.c b/mm/migrate.c
index 2731ac20ff33..52aa4770fbed 100644
--- a/mm/migrate.c
+++ b/mm/migrate.c
@@ -206,14 +206,10 @@ static bool try_to_unmap_unused(struct page_vma_mapped_walk *pvmw,
        if (dirty)
                return false;
 
-       pte_clear_not_present_full(pvmw->vma->vm_mm, pvmw->address, pvmw->pte, false);
-
-       if (userfaultfd_armed(pvmw->vma)) {
-               newpte = pte_mkspecial(pfn_pte(page_to_pfn(ZERO_PAGE(pvmw->address)),
-                                              pvmw->vma->vm_page_prot));
-               ptep_clear_flush(pvmw->vma, pvmw->address, pvmw->pte);
-               set_pte_at(pvmw->vma->vm_mm, pvmw->address, pvmw->pte, newpte);
-       }
+       newpte = pte_mkspecial(pfn_pte(page_to_pfn(ZERO_PAGE(pvmw->address)),
+                                       pvmw->vma->vm_page_prot));
+       ptep_clear_flush(pvmw->vma, pvmw->address, pvmw->pte);
+       set_pte_at(pvmw->vma->vm_mm, pvmw->address, pvmw->pte, newpte);
 
        dec_mm_counter(pvmw->vma->vm_mm, mm_counter(folio));
        return true;
David Hildenbrand July 30, 2024, 4:11 p.m. UTC | #4
On 30.07.24 17:19, Usama Arif wrote:
> 
> 
> On 30/07/2024 16:14, Usama Arif wrote:
>>
>>
>> On 30/07/2024 15:35, David Hildenbrand wrote:
>>> On 30.07.24 14:45, Usama Arif wrote:
>>>> The current upstream default policy for THP is always. However, Meta
>>>> uses madvise in production as the current THP=always policy vastly
>>>> overprovisions THPs in sparsely accessed memory areas, resulting in
>>>> excessive memory pressure and premature OOM killing.
>>>> Using madvise + relying on khugepaged has certain drawbacks over
>>>> THP=always. Using madvise hints mean THPs aren't "transparent" and
>>>> require userspace changes. Waiting for khugepaged to scan memory and
>>>> collapse pages into THP can be slow and unpredictable in terms of performance
>>>> (i.e. you dont know when the collapse will happen), while production
>>>> environments require predictable performance. If there is enough memory
>>>> available, its better for both performance and predictability to have
>>>> a THP from fault time, i.e. THP=always rather than wait for khugepaged
>>>> to collapse it, and deal with sparsely populated THPs when the system is
>>>> running out of memory.
>>>>
>>>> This patch-series is an attempt to mitigate the issue of running out of
>>>> memory when THP is always enabled. During runtime whenever a THP is being
>>>> faulted in or collapsed by khugepaged, the THP is added to a list.
>>>> Whenever memory reclaim happens, the kernel runs the deferred_split
>>>> shrinker which goes through the list and checks if the THP was underutilized,
>>>> i.e. how many of the base 4K pages of the entire THP were zero-filled.
>>>> If this number goes above a certain threshold, the shrinker will attempt
>>>> to split that THP. Then at remap time, the pages that were zero-filled are
>>>> not remapped, hence saving memory. This method avoids the downside of
>>>> wasting memory in areas where THP is sparsely filled when THP is always
>>>> enabled, while still providing the upside THPs like reduced TLB misses without
>>>> having to use madvise.
>>>>
>>>> Meta production workloads that were CPU bound (>99% CPU utilzation) were
>>>> tested with THP shrinker. The results after 2 hours are as follows:
>>>>
>>>>                               | THP=madvise |  THP=always   | THP=always
>>>>                               |             |               | + shrinker series
>>>>                               |             |               | + max_ptes_none=409
>>>> -----------------------------------------------------------------------------
>>>> Performance improvement     |      -      |    +1.8%      |     +1.7%
>>>> (over THP=madvise)          |             |               |
>>>> -----------------------------------------------------------------------------
>>>> Memory usage                |    54.6G    | 58.8G (+7.7%) |   55.9G (+2.4%)
>>>> -----------------------------------------------------------------------------
>>>> max_ptes_none=409 means that any THP that has more than 409 out of 512
>>>> (80%) zero filled filled pages will be split.
>>>>
>>>> To test out the patches, the below commands without the shrinker will
>>>> invoke OOM killer immediately and kill stress, but will not fail with
>>>> the shrinker:
>>>>
>>>> echo 450 > /sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none
>>>> mkdir /sys/fs/cgroup/test
>>>> echo $$ > /sys/fs/cgroup/test/cgroup.procs
>>>> echo 20M > /sys/fs/cgroup/test/memory.max
>>>> echo 0 > /sys/fs/cgroup/test/memory.swap.max
>>>> # allocate twice memory.max for each stress worker and touch 40/512 of
>>>> # each THP, i.e. vm-stride 50K.
>>>> # With the shrinker, max_ptes_none of 470 and below won't invoke OOM
>>>> # killer.
>>>> # Without the shrinker, OOM killer is invoked immediately irrespective
>>>> # of max_ptes_none value and kill stress.
>>>> stress --vm 1 --vm-bytes 40M --vm-stride 50K
>>>>
>>>> Patches 1-2 add back helper functions that were previously removed
>>>> to operate on page lists (needed by patch 3).
>>>> Patch 3 is an optimization to free zapped tail pages rather than
>>>> waiting for page reclaim or migration.
>>>> Patch 4 is a prerequisite for THP shrinker to not remap zero-filled
>>>> subpages when splitting THP.
>>>> Patches 6 adds support for THP shrinker.
>>>>
>>>> (This patch-series restarts the work on having a THP shrinker in kernel
>>>> originally done in
>>>> https://lore.kernel.org/all/cover.1667454613.git.alexlzhu@fb.com/.
>>>> The THP shrinker in this series is significantly different than the
>>>> original one, hence its labelled v1 (although the prerequisite to not
>>>> remap clean subpages is the same).)
>>>
>>> As shared previously, there is one issue with uffd (even when currently not active for a VMA!), where we must not zap present page table entries.
>>>
>>> Something that is always possible (assuming no GUP pins of course, which) is replacing the zero-filled subpages by shared zeropages.
>>>
>>> Is that being done in this patch set already, or are we creating pte_none() entries?
>>>
>>
>> I think thats done in Patch 4/6. In function try_to_unmap_unused, we have below which I think does what you are suggesting? i.e. point to shared zeropage and not clear pte for uffd armed vma.
>>
>> 	if (userfaultfd_armed(pvmw->vma)) {
>> 		newpte = pte_mkspecial(pfn_pte(page_to_pfn(ZERO_PAGE(pvmw->address)),
>> 					       pvmw->vma->vm_page_prot));
>> 		ptep_clear_flush(pvmw->vma, pvmw->address, pvmw->pte);
>> 		set_pte_at(pvmw->vma->vm_mm, pvmw->address, pvmw->pte, newpte);
>> 	}
> 
> 
> Ah are you suggesting userfaultfd_armed(pvmw->vma) will evaluate to false even if its uffd? I think something like below would work in that case.

I remember one ugly case in QEMU with postcopy live-migration where we 
must not zap zero-filled pages. I am not 100% regarding THP (if it could 
be enabled at that point), but imagine the following

1) mmap(), enable THP
2) Migrate a bunch of pages from the source during precopy (writing to
    the memory). Might end up creating THPs (during fault/khugepaged)
3) Register UFFD on the VMA
4) Disable new THPs from forming via MADV_NOHUGEPAGE on the VMA
5) Discard any pages that have been re-dirtied or not migrated yet
6) Migrate-on-demand any holes using uffd


If we discard zero-filled pages between 2) and 3) we might get wrong 
uffd notifications in 6 for pages that have already been migrated).

I'll have to check if that actually happens in that sequence in QEMU: if 
QEMU would disable THP right before 2) we would be safe. But I recall 
that it is not the case :/
Usama Arif July 30, 2024, 5:22 p.m. UTC | #5
On 30/07/2024 17:11, David Hildenbrand wrote:
> On 30.07.24 17:19, Usama Arif wrote:
>>
>>
>> On 30/07/2024 16:14, Usama Arif wrote:
>>>
>>>
>>> On 30/07/2024 15:35, David Hildenbrand wrote:
>>>> On 30.07.24 14:45, Usama Arif wrote:
>>>>> The current upstream default policy for THP is always. However, Meta
>>>>> uses madvise in production as the current THP=always policy vastly
>>>>> overprovisions THPs in sparsely accessed memory areas, resulting in
>>>>> excessive memory pressure and premature OOM killing.
>>>>> Using madvise + relying on khugepaged has certain drawbacks over
>>>>> THP=always. Using madvise hints mean THPs aren't "transparent" and
>>>>> require userspace changes. Waiting for khugepaged to scan memory and
>>>>> collapse pages into THP can be slow and unpredictable in terms of performance
>>>>> (i.e. you dont know when the collapse will happen), while production
>>>>> environments require predictable performance. If there is enough memory
>>>>> available, its better for both performance and predictability to have
>>>>> a THP from fault time, i.e. THP=always rather than wait for khugepaged
>>>>> to collapse it, and deal with sparsely populated THPs when the system is
>>>>> running out of memory.
>>>>>
>>>>> This patch-series is an attempt to mitigate the issue of running out of
>>>>> memory when THP is always enabled. During runtime whenever a THP is being
>>>>> faulted in or collapsed by khugepaged, the THP is added to a list.
>>>>> Whenever memory reclaim happens, the kernel runs the deferred_split
>>>>> shrinker which goes through the list and checks if the THP was underutilized,
>>>>> i.e. how many of the base 4K pages of the entire THP were zero-filled.
>>>>> If this number goes above a certain threshold, the shrinker will attempt
>>>>> to split that THP. Then at remap time, the pages that were zero-filled are
>>>>> not remapped, hence saving memory. This method avoids the downside of
>>>>> wasting memory in areas where THP is sparsely filled when THP is always
>>>>> enabled, while still providing the upside THPs like reduced TLB misses without
>>>>> having to use madvise.
>>>>>
>>>>> Meta production workloads that were CPU bound (>99% CPU utilzation) were
>>>>> tested with THP shrinker. The results after 2 hours are as follows:
>>>>>
>>>>>                               | THP=madvise |  THP=always   | THP=always
>>>>>                               |             |               | + shrinker series
>>>>>                               |             |               | + max_ptes_none=409
>>>>> -----------------------------------------------------------------------------
>>>>> Performance improvement     |      -      |    +1.8%      |     +1.7%
>>>>> (over THP=madvise)          |             |               |
>>>>> -----------------------------------------------------------------------------
>>>>> Memory usage                |    54.6G    | 58.8G (+7.7%) |   55.9G (+2.4%)
>>>>> -----------------------------------------------------------------------------
>>>>> max_ptes_none=409 means that any THP that has more than 409 out of 512
>>>>> (80%) zero filled filled pages will be split.
>>>>>
>>>>> To test out the patches, the below commands without the shrinker will
>>>>> invoke OOM killer immediately and kill stress, but will not fail with
>>>>> the shrinker:
>>>>>
>>>>> echo 450 > /sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none
>>>>> mkdir /sys/fs/cgroup/test
>>>>> echo $$ > /sys/fs/cgroup/test/cgroup.procs
>>>>> echo 20M > /sys/fs/cgroup/test/memory.max
>>>>> echo 0 > /sys/fs/cgroup/test/memory.swap.max
>>>>> # allocate twice memory.max for each stress worker and touch 40/512 of
>>>>> # each THP, i.e. vm-stride 50K.
>>>>> # With the shrinker, max_ptes_none of 470 and below won't invoke OOM
>>>>> # killer.
>>>>> # Without the shrinker, OOM killer is invoked immediately irrespective
>>>>> # of max_ptes_none value and kill stress.
>>>>> stress --vm 1 --vm-bytes 40M --vm-stride 50K
>>>>>
>>>>> Patches 1-2 add back helper functions that were previously removed
>>>>> to operate on page lists (needed by patch 3).
>>>>> Patch 3 is an optimization to free zapped tail pages rather than
>>>>> waiting for page reclaim or migration.
>>>>> Patch 4 is a prerequisite for THP shrinker to not remap zero-filled
>>>>> subpages when splitting THP.
>>>>> Patches 6 adds support for THP shrinker.
>>>>>
>>>>> (This patch-series restarts the work on having a THP shrinker in kernel
>>>>> originally done in
>>>>> https://lore.kernel.org/all/cover.1667454613.git.alexlzhu@fb.com/.
>>>>> The THP shrinker in this series is significantly different than the
>>>>> original one, hence its labelled v1 (although the prerequisite to not
>>>>> remap clean subpages is the same).)
>>>>
>>>> As shared previously, there is one issue with uffd (even when currently not active for a VMA!), where we must not zap present page table entries.
>>>>
>>>> Something that is always possible (assuming no GUP pins of course, which) is replacing the zero-filled subpages by shared zeropages.
>>>>
>>>> Is that being done in this patch set already, or are we creating pte_none() entries?
>>>>
>>>
>>> I think thats done in Patch 4/6. In function try_to_unmap_unused, we have below which I think does what you are suggesting? i.e. point to shared zeropage and not clear pte for uffd armed vma.
>>>
>>>     if (userfaultfd_armed(pvmw->vma)) {
>>>         newpte = pte_mkspecial(pfn_pte(page_to_pfn(ZERO_PAGE(pvmw->address)),
>>>                            pvmw->vma->vm_page_prot));
>>>         ptep_clear_flush(pvmw->vma, pvmw->address, pvmw->pte);
>>>         set_pte_at(pvmw->vma->vm_mm, pvmw->address, pvmw->pte, newpte);
>>>     }
>>
>>
>> Ah are you suggesting userfaultfd_armed(pvmw->vma) will evaluate to false even if its uffd? I think something like below would work in that case.
> 
> I remember one ugly case in QEMU with postcopy live-migration where we must not zap zero-filled pages. I am not 100% regarding THP (if it could be enabled at that point), but imagine the following
> 
> 1) mmap(), enable THP
> 2) Migrate a bunch of pages from the source during precopy (writing to
>    the memory). Might end up creating THPs (during fault/khugepaged)
> 3) Register UFFD on the VMA
> 4) Disable new THPs from forming via MADV_NOHUGEPAGE on the VMA
> 5) Discard any pages that have been re-dirtied or not migrated yet
> 6) Migrate-on-demand any holes using uffd
> 
> 
> If we discard zero-filled pages between 2) and 3) we might get wrong uffd notifications in 6 for pages that have already been migrated).
> 
> I'll have to check if that actually happens in that sequence in QEMU: if QEMU would disable THP right before 2) we would be safe. But I recall that it is not the case :/
> 
> 

Thanks for the example!

Just to understand the issue better, as I am not very familiar with live-migration code, the problem is only for zero-filled pages that were migrated, right? If a THP is created and a subpage of it was a zero-page that was migrated and its split before VMA is armed with uffd, userfaultfd_armed(pvmw->vma) will return false when splitting and it will become pte_none. And afterwards when the destination faults on it, uffd will see that its pte_clear and will request the zero-page back from source. Uffd will then have to get the page again from source.

If I understand the example correctly, the below diff over patch 6 should be good? i.e. just point to the empty_zero_page instead of doing pte_clear. This should still use the same amount of memory, although ptep_clear_flush means it might be slighly more expensive.

diff --git a/mm/migrate.c b/mm/migrate.c
index 2731ac20ff33..52aa4770fbed 100644
--- a/mm/migrate.c
+++ b/mm/migrate.c
@@ -206,14 +206,10 @@ static bool try_to_unmap_unused(struct page_vma_mapped_walk *pvmw,
        if (dirty)
                return false;
 
-       pte_clear_not_present_full(pvmw->vma->vm_mm, pvmw->address, pvmw->pte, false);
-
-       if (userfaultfd_armed(pvmw->vma)) {
-               newpte = pte_mkspecial(pfn_pte(page_to_pfn(ZERO_PAGE(pvmw->address)),
-                                              pvmw->vma->vm_page_prot));
-               ptep_clear_flush(pvmw->vma, pvmw->address, pvmw->pte);
-               set_pte_at(pvmw->vma->vm_mm, pvmw->address, pvmw->pte, newpte);
-       }
+       newpte = pte_mkspecial(pfn_pte(page_to_pfn(ZERO_PAGE(pvmw->address)),
+                                       pvmw->vma->vm_page_prot));
+       ptep_clear_flush(pvmw->vma, pvmw->address, pvmw->pte);
+       set_pte_at(pvmw->vma->vm_mm, pvmw->address, pvmw->pte, newpte);
 
        dec_mm_counter(pvmw->vma->vm_mm, mm_counter(folio));
        return true;
David Hildenbrand July 30, 2024, 8:25 p.m. UTC | #6
On 30.07.24 19:22, Usama Arif wrote:
> 
> 
> On 30/07/2024 17:11, David Hildenbrand wrote:
>> On 30.07.24 17:19, Usama Arif wrote:
>>>
>>>
>>> On 30/07/2024 16:14, Usama Arif wrote:
>>>>
>>>>
>>>> On 30/07/2024 15:35, David Hildenbrand wrote:
>>>>> On 30.07.24 14:45, Usama Arif wrote:
>>>>>> The current upstream default policy for THP is always. However, Meta
>>>>>> uses madvise in production as the current THP=always policy vastly
>>>>>> overprovisions THPs in sparsely accessed memory areas, resulting in
>>>>>> excessive memory pressure and premature OOM killing.
>>>>>> Using madvise + relying on khugepaged has certain drawbacks over
>>>>>> THP=always. Using madvise hints mean THPs aren't "transparent" and
>>>>>> require userspace changes. Waiting for khugepaged to scan memory and
>>>>>> collapse pages into THP can be slow and unpredictable in terms of performance
>>>>>> (i.e. you dont know when the collapse will happen), while production
>>>>>> environments require predictable performance. If there is enough memory
>>>>>> available, its better for both performance and predictability to have
>>>>>> a THP from fault time, i.e. THP=always rather than wait for khugepaged
>>>>>> to collapse it, and deal with sparsely populated THPs when the system is
>>>>>> running out of memory.
>>>>>>
>>>>>> This patch-series is an attempt to mitigate the issue of running out of
>>>>>> memory when THP is always enabled. During runtime whenever a THP is being
>>>>>> faulted in or collapsed by khugepaged, the THP is added to a list.
>>>>>> Whenever memory reclaim happens, the kernel runs the deferred_split
>>>>>> shrinker which goes through the list and checks if the THP was underutilized,
>>>>>> i.e. how many of the base 4K pages of the entire THP were zero-filled.
>>>>>> If this number goes above a certain threshold, the shrinker will attempt
>>>>>> to split that THP. Then at remap time, the pages that were zero-filled are
>>>>>> not remapped, hence saving memory. This method avoids the downside of
>>>>>> wasting memory in areas where THP is sparsely filled when THP is always
>>>>>> enabled, while still providing the upside THPs like reduced TLB misses without
>>>>>> having to use madvise.
>>>>>>
>>>>>> Meta production workloads that were CPU bound (>99% CPU utilzation) were
>>>>>> tested with THP shrinker. The results after 2 hours are as follows:
>>>>>>
>>>>>>                                | THP=madvise |  THP=always   | THP=always
>>>>>>                                |             |               | + shrinker series
>>>>>>                                |             |               | + max_ptes_none=409
>>>>>> -----------------------------------------------------------------------------
>>>>>> Performance improvement     |      -      |    +1.8%      |     +1.7%
>>>>>> (over THP=madvise)          |             |               |
>>>>>> -----------------------------------------------------------------------------
>>>>>> Memory usage                |    54.6G    | 58.8G (+7.7%) |   55.9G (+2.4%)
>>>>>> -----------------------------------------------------------------------------
>>>>>> max_ptes_none=409 means that any THP that has more than 409 out of 512
>>>>>> (80%) zero filled filled pages will be split.
>>>>>>
>>>>>> To test out the patches, the below commands without the shrinker will
>>>>>> invoke OOM killer immediately and kill stress, but will not fail with
>>>>>> the shrinker:
>>>>>>
>>>>>> echo 450 > /sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none
>>>>>> mkdir /sys/fs/cgroup/test
>>>>>> echo $$ > /sys/fs/cgroup/test/cgroup.procs
>>>>>> echo 20M > /sys/fs/cgroup/test/memory.max
>>>>>> echo 0 > /sys/fs/cgroup/test/memory.swap.max
>>>>>> # allocate twice memory.max for each stress worker and touch 40/512 of
>>>>>> # each THP, i.e. vm-stride 50K.
>>>>>> # With the shrinker, max_ptes_none of 470 and below won't invoke OOM
>>>>>> # killer.
>>>>>> # Without the shrinker, OOM killer is invoked immediately irrespective
>>>>>> # of max_ptes_none value and kill stress.
>>>>>> stress --vm 1 --vm-bytes 40M --vm-stride 50K
>>>>>>
>>>>>> Patches 1-2 add back helper functions that were previously removed
>>>>>> to operate on page lists (needed by patch 3).
>>>>>> Patch 3 is an optimization to free zapped tail pages rather than
>>>>>> waiting for page reclaim or migration.
>>>>>> Patch 4 is a prerequisite for THP shrinker to not remap zero-filled
>>>>>> subpages when splitting THP.
>>>>>> Patches 6 adds support for THP shrinker.
>>>>>>
>>>>>> (This patch-series restarts the work on having a THP shrinker in kernel
>>>>>> originally done in
>>>>>> https://lore.kernel.org/all/cover.1667454613.git.alexlzhu@fb.com/.
>>>>>> The THP shrinker in this series is significantly different than the
>>>>>> original one, hence its labelled v1 (although the prerequisite to not
>>>>>> remap clean subpages is the same).)
>>>>>
>>>>> As shared previously, there is one issue with uffd (even when currently not active for a VMA!), where we must not zap present page table entries.
>>>>>
>>>>> Something that is always possible (assuming no GUP pins of course, which) is replacing the zero-filled subpages by shared zeropages.
>>>>>
>>>>> Is that being done in this patch set already, or are we creating pte_none() entries?
>>>>>
>>>>
>>>> I think thats done in Patch 4/6. In function try_to_unmap_unused, we have below which I think does what you are suggesting? i.e. point to shared zeropage and not clear pte for uffd armed vma.
>>>>
>>>>      if (userfaultfd_armed(pvmw->vma)) {
>>>>          newpte = pte_mkspecial(pfn_pte(page_to_pfn(ZERO_PAGE(pvmw->address)),
>>>>                             pvmw->vma->vm_page_prot));
>>>>          ptep_clear_flush(pvmw->vma, pvmw->address, pvmw->pte);
>>>>          set_pte_at(pvmw->vma->vm_mm, pvmw->address, pvmw->pte, newpte);
>>>>      }
>>>
>>>
>>> Ah are you suggesting userfaultfd_armed(pvmw->vma) will evaluate to false even if its uffd? I think something like below would work in that case.
>>
>> I remember one ugly case in QEMU with postcopy live-migration where we must not zap zero-filled pages. I am not 100% regarding THP (if it could be enabled at that point), but imagine the following
>>
>> 1) mmap(), enable THP
>> 2) Migrate a bunch of pages from the source during precopy (writing to
>>     the memory). Might end up creating THPs (during fault/khugepaged)
>> 3) Register UFFD on the VMA
>> 4) Disable new THPs from forming via MADV_NOHUGEPAGE on the VMA
>> 5) Discard any pages that have been re-dirtied or not migrated yet
>> 6) Migrate-on-demand any holes using uffd
>>
>>
>> If we discard zero-filled pages between 2) and 3) we might get wrong uffd notifications in 6 for pages that have already been migrated).
>>
>> I'll have to check if that actually happens in that sequence in QEMU: if QEMU would disable THP right before 2) we would be safe. But I recall that it is not the case :/
>>
>>
> 
> Thanks for the example!
> 
> Just to understand the issue better, as I am not very familiar with live-migration code, the problem is only for zero-filled pages that were migrated, right? If a THP is created and a subpage of it was a zero-page that was migrated and its split before VMA is armed with uffd, userfaultfd_armed(pvmw->vma) will return false when splitting and it will become pte_none. And afterwards when the destination faults on it, uffd will see that its pte_clear and will request the zero-page back from source. Uffd will then have to get the page again from source.

Something like that, but I recall that if it detects that it already 
migrated the page stuff will go wrong.

IIRC QEMU maintains receive bitmaps about which pages it already 
received+placed. Staring at migrate_send_rp_req_pages(), QEMU ignores 
the uffd notification if it finds that the page was already received and 
would not ask the source again.

So if we turn some PTEs that map zeroed-pages into pte-none we could run 
into trouble, because we will never try requesting/placing that page 
again and our VM will just be stuck forever.

I wonder if other uffd users could similarly be affected. But maybe they 
don't place pages into the VMA before registering uffd.

I'll try to double-check when QEMU would disable THP. But it could also 
be that that behavior changed between QEMU versions.

> 
> If I understand the example correctly, the below diff over patch 6 should be good? i.e. just point to the empty_zero_page instead of doing pte_clear. This should still use the same amount of memory, although ptep_clear_flush means it might be slighly more expensive.

There are rare cases where we must not use the shared zeropage 
(mm_forbids_zeropage()), that must be handled.

I assume we could optimize here if uffd is not configured in. Further, 
what QEMU does is sense right at the beginning by temporarily 
registering uffd on some other VMA if it is even supported. That could 
be used as an indication ("ever used uffd -> don't turn PTEs none"). But 
again, no idea what other uffd users might be relying on :/

In I wonder if some applications could rely on anon memory not suddenly 
"vanishing" form the PTEs (for example relying on pagemap like 
tools/testing/selftests/mm/cow.c) would. I don't think a lot of 
applications would do that, though.
Usama Arif July 31, 2024, 5:01 p.m. UTC | #7
On 30/07/2024 21:25, David Hildenbrand wrote:
> On 30.07.24 19:22, Usama Arif wrote:
>>
>>
>> On 30/07/2024 17:11, David Hildenbrand wrote:
>>> On 30.07.24 17:19, Usama Arif wrote:
>>>>
>>>>
>>>> On 30/07/2024 16:14, Usama Arif wrote:
>>>>>
>>>>>
>>>>> On 30/07/2024 15:35, David Hildenbrand wrote:
>>>>>> On 30.07.24 14:45, Usama Arif wrote:
>>>>>>> The current upstream default policy for THP is always. However, Meta
>>>>>>> uses madvise in production as the current THP=always policy vastly
>>>>>>> overprovisions THPs in sparsely accessed memory areas, resulting in
>>>>>>> excessive memory pressure and premature OOM killing.
>>>>>>> Using madvise + relying on khugepaged has certain drawbacks over
>>>>>>> THP=always. Using madvise hints mean THPs aren't "transparent" and
>>>>>>> require userspace changes. Waiting for khugepaged to scan memory and
>>>>>>> collapse pages into THP can be slow and unpredictable in terms of performance
>>>>>>> (i.e. you dont know when the collapse will happen), while production
>>>>>>> environments require predictable performance. If there is enough memory
>>>>>>> available, its better for both performance and predictability to have
>>>>>>> a THP from fault time, i.e. THP=always rather than wait for khugepaged
>>>>>>> to collapse it, and deal with sparsely populated THPs when the system is
>>>>>>> running out of memory.
>>>>>>>
>>>>>>> This patch-series is an attempt to mitigate the issue of running out of
>>>>>>> memory when THP is always enabled. During runtime whenever a THP is being
>>>>>>> faulted in or collapsed by khugepaged, the THP is added to a list.
>>>>>>> Whenever memory reclaim happens, the kernel runs the deferred_split
>>>>>>> shrinker which goes through the list and checks if the THP was underutilized,
>>>>>>> i.e. how many of the base 4K pages of the entire THP were zero-filled.
>>>>>>> If this number goes above a certain threshold, the shrinker will attempt
>>>>>>> to split that THP. Then at remap time, the pages that were zero-filled are
>>>>>>> not remapped, hence saving memory. This method avoids the downside of
>>>>>>> wasting memory in areas where THP is sparsely filled when THP is always
>>>>>>> enabled, while still providing the upside THPs like reduced TLB misses without
>>>>>>> having to use madvise.
>>>>>>>
>>>>>>> Meta production workloads that were CPU bound (>99% CPU utilzation) were
>>>>>>> tested with THP shrinker. The results after 2 hours are as follows:
>>>>>>>
>>>>>>>                                | THP=madvise |  THP=always   | THP=always
>>>>>>>                                |             |               | + shrinker series
>>>>>>>                                |             |               | + max_ptes_none=409
>>>>>>> -----------------------------------------------------------------------------
>>>>>>> Performance improvement     |      -      |    +1.8%      |     +1.7%
>>>>>>> (over THP=madvise)          |             |               |
>>>>>>> -----------------------------------------------------------------------------
>>>>>>> Memory usage                |    54.6G    | 58.8G (+7.7%) |   55.9G (+2.4%)
>>>>>>> -----------------------------------------------------------------------------
>>>>>>> max_ptes_none=409 means that any THP that has more than 409 out of 512
>>>>>>> (80%) zero filled filled pages will be split.
>>>>>>>
>>>>>>> To test out the patches, the below commands without the shrinker will
>>>>>>> invoke OOM killer immediately and kill stress, but will not fail with
>>>>>>> the shrinker:
>>>>>>>
>>>>>>> echo 450 > /sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none
>>>>>>> mkdir /sys/fs/cgroup/test
>>>>>>> echo $$ > /sys/fs/cgroup/test/cgroup.procs
>>>>>>> echo 20M > /sys/fs/cgroup/test/memory.max
>>>>>>> echo 0 > /sys/fs/cgroup/test/memory.swap.max
>>>>>>> # allocate twice memory.max for each stress worker and touch 40/512 of
>>>>>>> # each THP, i.e. vm-stride 50K.
>>>>>>> # With the shrinker, max_ptes_none of 470 and below won't invoke OOM
>>>>>>> # killer.
>>>>>>> # Without the shrinker, OOM killer is invoked immediately irrespective
>>>>>>> # of max_ptes_none value and kill stress.
>>>>>>> stress --vm 1 --vm-bytes 40M --vm-stride 50K
>>>>>>>
>>>>>>> Patches 1-2 add back helper functions that were previously removed
>>>>>>> to operate on page lists (needed by patch 3).
>>>>>>> Patch 3 is an optimization to free zapped tail pages rather than
>>>>>>> waiting for page reclaim or migration.
>>>>>>> Patch 4 is a prerequisite for THP shrinker to not remap zero-filled
>>>>>>> subpages when splitting THP.
>>>>>>> Patches 6 adds support for THP shrinker.
>>>>>>>
>>>>>>> (This patch-series restarts the work on having a THP shrinker in kernel
>>>>>>> originally done in
>>>>>>> https://lore.kernel.org/all/cover.1667454613.git.alexlzhu@fb.com/.
>>>>>>> The THP shrinker in this series is significantly different than the
>>>>>>> original one, hence its labelled v1 (although the prerequisite to not
>>>>>>> remap clean subpages is the same).)
>>>>>>
>>>>>> As shared previously, there is one issue with uffd (even when currently not active for a VMA!), where we must not zap present page table entries.
>>>>>>
>>>>>> Something that is always possible (assuming no GUP pins of course, which) is replacing the zero-filled subpages by shared zeropages.
>>>>>>
>>>>>> Is that being done in this patch set already, or are we creating pte_none() entries?
>>>>>>
>>>>>
>>>>> I think thats done in Patch 4/6. In function try_to_unmap_unused, we have below which I think does what you are suggesting? i.e. point to shared zeropage and not clear pte for uffd armed vma.
>>>>>
>>>>>      if (userfaultfd_armed(pvmw->vma)) {
>>>>>          newpte = pte_mkspecial(pfn_pte(page_to_pfn(ZERO_PAGE(pvmw->address)),
>>>>>                             pvmw->vma->vm_page_prot));
>>>>>          ptep_clear_flush(pvmw->vma, pvmw->address, pvmw->pte);
>>>>>          set_pte_at(pvmw->vma->vm_mm, pvmw->address, pvmw->pte, newpte);
>>>>>      }
>>>>
>>>>
>>>> Ah are you suggesting userfaultfd_armed(pvmw->vma) will evaluate to false even if its uffd? I think something like below would work in that case.
>>>
>>> I remember one ugly case in QEMU with postcopy live-migration where we must not zap zero-filled pages. I am not 100% regarding THP (if it could be enabled at that point), but imagine the following
>>>
>>> 1) mmap(), enable THP
>>> 2) Migrate a bunch of pages from the source during precopy (writing to
>>>     the memory). Might end up creating THPs (during fault/khugepaged)
>>> 3) Register UFFD on the VMA
>>> 4) Disable new THPs from forming via MADV_NOHUGEPAGE on the VMA
>>> 5) Discard any pages that have been re-dirtied or not migrated yet
>>> 6) Migrate-on-demand any holes using uffd
>>>
>>>
>>> If we discard zero-filled pages between 2) and 3) we might get wrong uffd notifications in 6 for pages that have already been migrated).
>>>
>>> I'll have to check if that actually happens in that sequence in QEMU: if QEMU would disable THP right before 2) we would be safe. But I recall that it is not the case :/
>>>
>>>
>>
>> Thanks for the example!
>>
>> Just to understand the issue better, as I am not very familiar with live-migration code, the problem is only for zero-filled pages that were migrated, right? If a THP is created and a subpage of it was a zero-page that was migrated and its split before VMA is armed with uffd, userfaultfd_armed(pvmw->vma) will return false when splitting and it will become pte_none. And afterwards when the destination faults on it, uffd will see that its pte_clear and will request the zero-page back from source. Uffd will then have to get the page again from source.
> 
> Something like that, but I recall that if it detects that it already migrated the page stuff will go wrong.
> 
> IIRC QEMU maintains receive bitmaps about which pages it already received+placed. Staring at migrate_send_rp_req_pages(), QEMU ignores the uffd notification if it finds that the page was already received and would not ask the source again.
> 
> So if we turn some PTEs that map zeroed-pages into pte-none we could run into trouble, because we will never try requesting/placing that page again and our VM will just be stuck forever.
> 
> I wonder if other uffd users could similarly be affected. But maybe they don't place pages into the VMA before registering uffd.
> 
> I'll try to double-check when QEMU would disable THP. But it could also be that that behavior changed between QEMU versions.
> 
>>
>> If I understand the example correctly, the below diff over patch 6 should be good? i.e. just point to the empty_zero_page instead of doing pte_clear. This should still use the same amount of memory, although ptep_clear_flush means it might be slighly more expensive.
> 
> There are rare cases where we must not use the shared zeropage (mm_forbids_zeropage()), that must be handled.
> 
> I assume we could optimize here if uffd is not configured in. Further, what QEMU does is sense right at the beginning by temporarily registering uffd on some other VMA if it is even supported. That could be used as an indication ("ever used uffd -> don't turn PTEs none"). But again, no idea what other uffd users might be relying on :/
> 
> In I wonder if some applications could rely on anon memory not suddenly "vanishing" form the PTEs (for example relying on pagemap like tools/testing/selftests/mm/cow.c) would. I don't think a lot of applications would do that, though.
> 


I had a deeper look at how QEMU handles migration and at how kernel handles THPs in other places with respect to uffd. I hope I understood the migration code correctly :)

QEMU:
There are 2 types of "migrations" [1], migration_thread and bg_migration_thread
- migration_thread supports postcopy live migration, but doesn't use uffd. So postcopy live migration doesn't use uffd as far as I can tell looking at the function [2]. 
- bg_migration_thread [3]: this isn't really live migration, but background snapshot. There is no postcopy and it works similar to pre-copy. From what I understand, uffd is registered before any pages are migrated [4].

So the way these 2 threads work in qemu is: its either postcopy without uffd, or precopy-style snapshot with uffd registered at the start, then the current patch in this series is good, i.e. only use zeropage if userfaultfd_armed(pvmw->vma), otherwise pte_clear. I can't see postcopy with uffd anywhere in qemu which doesn all the steps 1-6 that you mentioned above, but I might not be looking at the right place?

Kernel:
From a kernel perspective, if we look at khugepaged [5], it just takes into account if the vma is currently registered as uffd. So maybe this scenario can happen?
1) THP is enabled on VMA, UFFD is not yet registered.
2) dest accesses a 4K page (lets call it 4Kpage1), khugepaged collapses that page into 2M THP as the following 511 4K pages were pte_none [5], as VMA is not uffd armed yet.
3) UFFD is registered + MADV_NOHUGEPAGE is set. No further collapse will happen, but the THPs that were created in step 2 will remain. MADV_NOHUGEPAGE doesn't split existing THP created in 2.
4) dest tries to access 4Kpage2, the address right after 4Kpage1. As khugepaged collapsed it, there won't be a pagefault, and dest will read it as a zero-filled page, even if UFFD handler wanted to give some non-zero data filled page. I think anyone who wrote the uffd handler would want there to be a pagefault and give the non-zero data filled page and would not expect dest to see a zero-filled page.

What I am trying to say with the above example is:
- UFFD registeration + MADV_NOHUGEPAGE should be done by all applications before any data in the region is accessed. Using THPs and accessing data before UFFD registeration + MADV_NOHUGEPAGE can lead to unexpected behaviour and is wrong?
- even in the current kernel code in other places like khugepaged, its only checked if uffd is enabled currently. It is not tracked if it was ever enabled on any VMA.

Thanks for pointing to mm_forbids_zeropage. Incorporating that into the code, and if I am (hopefully :)) right about qemu and kernel above, then I believe the right code should be:

	if (userfaultfd_armed(pvmw->vma) && mm_forbids_zeropage(pvmw->vma->vm_mm))
		return false;

	if (!userfaultfd_armed(pvmw->vma)) {
		pte_clear_not_present_full(pvmw->vma->vm_mm, pvmw->address, pvmw->pte, false);
	} else {
		newpte = pte_mkspecial(pfn_pte(page_to_pfn(ZERO_PAGE(pvmw->address)),
					       pvmw->vma->vm_page_prot));
		ptep_clear_flush(pvmw->vma, pvmw->address, pvmw->pte);
		set_pte_at(pvmw->vma->vm_mm, pvmw->address, pvmw->pte, newpte);
	}
 


[1] https://github.com/qemu/qemu/blob/4e56e89d6c81589cc47cf5811f570c67889bd18a/migration/migration.c#L3817
[2] https://github.com/qemu/qemu/blob/4e56e89d6c81589cc47cf5811f570c67889bd18a/migration/migration.c#L3455
[3] https://github.com/qemu/qemu/blob/4e56e89d6c81589cc47cf5811f570c67889bd18a/migration/migration.c#L3591
[4] https://github.com/qemu/qemu/blob/4e56e89d6c81589cc47cf5811f570c67889bd18a/migration/migration.c#L3675
[5] https://github.com/torvalds/linux/blob/master/mm/khugepaged.c#L1307
David Hildenbrand July 31, 2024, 5:51 p.m. UTC | #8
On 31.07.24 19:01, Usama Arif wrote:
> 
> 
> On 30/07/2024 21:25, David Hildenbrand wrote:
>> On 30.07.24 19:22, Usama Arif wrote:
>>>
>>>
>>> On 30/07/2024 17:11, David Hildenbrand wrote:
>>>> On 30.07.24 17:19, Usama Arif wrote:
>>>>>
>>>>>
>>>>> On 30/07/2024 16:14, Usama Arif wrote:
>>>>>>
>>>>>>
>>>>>> On 30/07/2024 15:35, David Hildenbrand wrote:
>>>>>>> On 30.07.24 14:45, Usama Arif wrote:
>>>>>>>> The current upstream default policy for THP is always. However, Meta
>>>>>>>> uses madvise in production as the current THP=always policy vastly
>>>>>>>> overprovisions THPs in sparsely accessed memory areas, resulting in
>>>>>>>> excessive memory pressure and premature OOM killing.
>>>>>>>> Using madvise + relying on khugepaged has certain drawbacks over
>>>>>>>> THP=always. Using madvise hints mean THPs aren't "transparent" and
>>>>>>>> require userspace changes. Waiting for khugepaged to scan memory and
>>>>>>>> collapse pages into THP can be slow and unpredictable in terms of performance
>>>>>>>> (i.e. you dont know when the collapse will happen), while production
>>>>>>>> environments require predictable performance. If there is enough memory
>>>>>>>> available, its better for both performance and predictability to have
>>>>>>>> a THP from fault time, i.e. THP=always rather than wait for khugepaged
>>>>>>>> to collapse it, and deal with sparsely populated THPs when the system is
>>>>>>>> running out of memory.
>>>>>>>>
>>>>>>>> This patch-series is an attempt to mitigate the issue of running out of
>>>>>>>> memory when THP is always enabled. During runtime whenever a THP is being
>>>>>>>> faulted in or collapsed by khugepaged, the THP is added to a list.
>>>>>>>> Whenever memory reclaim happens, the kernel runs the deferred_split
>>>>>>>> shrinker which goes through the list and checks if the THP was underutilized,
>>>>>>>> i.e. how many of the base 4K pages of the entire THP were zero-filled.
>>>>>>>> If this number goes above a certain threshold, the shrinker will attempt
>>>>>>>> to split that THP. Then at remap time, the pages that were zero-filled are
>>>>>>>> not remapped, hence saving memory. This method avoids the downside of
>>>>>>>> wasting memory in areas where THP is sparsely filled when THP is always
>>>>>>>> enabled, while still providing the upside THPs like reduced TLB misses without
>>>>>>>> having to use madvise.
>>>>>>>>
>>>>>>>> Meta production workloads that were CPU bound (>99% CPU utilzation) were
>>>>>>>> tested with THP shrinker. The results after 2 hours are as follows:
>>>>>>>>
>>>>>>>>                                 | THP=madvise |  THP=always   | THP=always
>>>>>>>>                                 |             |               | + shrinker series
>>>>>>>>                                 |             |               | + max_ptes_none=409
>>>>>>>> -----------------------------------------------------------------------------
>>>>>>>> Performance improvement     |      -      |    +1.8%      |     +1.7%
>>>>>>>> (over THP=madvise)          |             |               |
>>>>>>>> -----------------------------------------------------------------------------
>>>>>>>> Memory usage                |    54.6G    | 58.8G (+7.7%) |   55.9G (+2.4%)
>>>>>>>> -----------------------------------------------------------------------------
>>>>>>>> max_ptes_none=409 means that any THP that has more than 409 out of 512
>>>>>>>> (80%) zero filled filled pages will be split.
>>>>>>>>
>>>>>>>> To test out the patches, the below commands without the shrinker will
>>>>>>>> invoke OOM killer immediately and kill stress, but will not fail with
>>>>>>>> the shrinker:
>>>>>>>>
>>>>>>>> echo 450 > /sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none
>>>>>>>> mkdir /sys/fs/cgroup/test
>>>>>>>> echo $$ > /sys/fs/cgroup/test/cgroup.procs
>>>>>>>> echo 20M > /sys/fs/cgroup/test/memory.max
>>>>>>>> echo 0 > /sys/fs/cgroup/test/memory.swap.max
>>>>>>>> # allocate twice memory.max for each stress worker and touch 40/512 of
>>>>>>>> # each THP, i.e. vm-stride 50K.
>>>>>>>> # With the shrinker, max_ptes_none of 470 and below won't invoke OOM
>>>>>>>> # killer.
>>>>>>>> # Without the shrinker, OOM killer is invoked immediately irrespective
>>>>>>>> # of max_ptes_none value and kill stress.
>>>>>>>> stress --vm 1 --vm-bytes 40M --vm-stride 50K
>>>>>>>>
>>>>>>>> Patches 1-2 add back helper functions that were previously removed
>>>>>>>> to operate on page lists (needed by patch 3).
>>>>>>>> Patch 3 is an optimization to free zapped tail pages rather than
>>>>>>>> waiting for page reclaim or migration.
>>>>>>>> Patch 4 is a prerequisite for THP shrinker to not remap zero-filled
>>>>>>>> subpages when splitting THP.
>>>>>>>> Patches 6 adds support for THP shrinker.
>>>>>>>>
>>>>>>>> (This patch-series restarts the work on having a THP shrinker in kernel
>>>>>>>> originally done in
>>>>>>>> https://lore.kernel.org/all/cover.1667454613.git.alexlzhu@fb.com/.
>>>>>>>> The THP shrinker in this series is significantly different than the
>>>>>>>> original one, hence its labelled v1 (although the prerequisite to not
>>>>>>>> remap clean subpages is the same).)
>>>>>>>
>>>>>>> As shared previously, there is one issue with uffd (even when currently not active for a VMA!), where we must not zap present page table entries.
>>>>>>>
>>>>>>> Something that is always possible (assuming no GUP pins of course, which) is replacing the zero-filled subpages by shared zeropages.
>>>>>>>
>>>>>>> Is that being done in this patch set already, or are we creating pte_none() entries?
>>>>>>>
>>>>>>
>>>>>> I think thats done in Patch 4/6. In function try_to_unmap_unused, we have below which I think does what you are suggesting? i.e. point to shared zeropage and not clear pte for uffd armed vma.
>>>>>>
>>>>>>       if (userfaultfd_armed(pvmw->vma)) {
>>>>>>           newpte = pte_mkspecial(pfn_pte(page_to_pfn(ZERO_PAGE(pvmw->address)),
>>>>>>                              pvmw->vma->vm_page_prot));
>>>>>>           ptep_clear_flush(pvmw->vma, pvmw->address, pvmw->pte);
>>>>>>           set_pte_at(pvmw->vma->vm_mm, pvmw->address, pvmw->pte, newpte);
>>>>>>       }
>>>>>
>>>>>
>>>>> Ah are you suggesting userfaultfd_armed(pvmw->vma) will evaluate to false even if its uffd? I think something like below would work in that case.
>>>>
>>>> I remember one ugly case in QEMU with postcopy live-migration where we must not zap zero-filled pages. I am not 100% regarding THP (if it could be enabled at that point), but imagine the following
>>>>
>>>> 1) mmap(), enable THP
>>>> 2) Migrate a bunch of pages from the source during precopy (writing to
>>>>      the memory). Might end up creating THPs (during fault/khugepaged)
>>>> 3) Register UFFD on the VMA
>>>> 4) Disable new THPs from forming via MADV_NOHUGEPAGE on the VMA
>>>> 5) Discard any pages that have been re-dirtied or not migrated yet
>>>> 6) Migrate-on-demand any holes using uffd
>>>>
>>>>
>>>> If we discard zero-filled pages between 2) and 3) we might get wrong uffd notifications in 6 for pages that have already been migrated).
>>>>
>>>> I'll have to check if that actually happens in that sequence in QEMU: if QEMU would disable THP right before 2) we would be safe. But I recall that it is not the case :/
>>>>
>>>>
>>>
>>> Thanks for the example!
>>>
>>> Just to understand the issue better, as I am not very familiar with live-migration code, the problem is only for zero-filled pages that were migrated, right? If a THP is created and a subpage of it was a zero-page that was migrated and its split before VMA is armed with uffd, userfaultfd_armed(pvmw->vma) will return false when splitting and it will become pte_none. And afterwards when the destination faults on it, uffd will see that its pte_clear and will request the zero-page back from source. Uffd will then have to get the page again from source.
>>
>> Something like that, but I recall that if it detects that it already migrated the page stuff will go wrong.
>>
>> IIRC QEMU maintains receive bitmaps about which pages it already received+placed. Staring at migrate_send_rp_req_pages(), QEMU ignores the uffd notification if it finds that the page was already received and would not ask the source again.
>>
>> So if we turn some PTEs that map zeroed-pages into pte-none we could run into trouble, because we will never try requesting/placing that page again and our VM will just be stuck forever.
>>
>> I wonder if other uffd users could similarly be affected. But maybe they don't place pages into the VMA before registering uffd.
>>
>> I'll try to double-check when QEMU would disable THP. But it could also be that that behavior changed between QEMU versions.
>>
>>>
>>> If I understand the example correctly, the below diff over patch 6 should be good? i.e. just point to the empty_zero_page instead of doing pte_clear. This should still use the same amount of memory, although ptep_clear_flush means it might be slighly more expensive.
>>
>> There are rare cases where we must not use the shared zeropage (mm_forbids_zeropage()), that must be handled.
>>
>> I assume we could optimize here if uffd is not configured in. Further, what QEMU does is sense right at the beginning by temporarily registering uffd on some other VMA if it is even supported. That could be used as an indication ("ever used uffd -> don't turn PTEs none"). But again, no idea what other uffd users might be relying on :/
>>
>> In I wonder if some applications could rely on anon memory not suddenly "vanishing" form the PTEs (for example relying on pagemap like tools/testing/selftests/mm/cow.c) would. I don't think a lot of applications would do that, though.
>>
> 
> 
> I had a deeper look at how QEMU handles migration and at how kernel handles THPs in other places with respect to uffd. I hope I understood the migration code correctly :)
> 
> QEMU:
> There are 2 types of "migrations" [1], migration_thread and bg_migration_thread
> - migration_thread supports postcopy live migration, but doesn't use uffd. So postcopy live migration doesn't use uffd as far as I can tell looking at the function [2].
> - bg_migration_thread [3]: this isn't really live migration, but background snapshot. There is no postcopy and it works similar to pre-copy. From what I understand, uffd is registered before any pages are migrated [4].
> 

Ignore the second. What QEMU ends up using is precopy + postcopy. You 
can start precopy migration and decide at some point that you want to 
switch to postcopy (this is what I trigger below, let me know if you 
want a simple way to reproduce it).

> So the way these 2 threads work in qemu is: its either postcopy without uffd, or precopy-style snapshot with uffd registered at the start, then the current patch in this series is good, i.e. only use zeropage if userfaultfd_armed(pvmw->vma), otherwise pte_clear. I can't see postcopy with uffd anywhere in qemu which doesn all the steps 1-6 that you mentioned above, but I might not be looking at the right place?

"I can't see postcopy with uffd anywhere" -- most of it lives in 
migration/postcopy-ram.c.

See postcopy_ram_incoming_setup() where most of the UFFD setup logic 
happens.

> 
> Kernel:
>  From a kernel perspective, if we look at khugepaged [5], it just takes into account if the vma is currently registered as uffd. So maybe this scenario can happen?
> 1) THP is enabled on VMA, UFFD is not yet registered.
> 2) dest accesses a 4K page (lets call it 4Kpage1), khugepaged collapses that page into 2M THP as the following 511 4K pages were pte_none [5], as VMA is not uffd armed yet.
> 3) UFFD is registered + MADV_NOHUGEPAGE is set. No further collapse will happen, but the THPs that were created in step 2 will remain. MADV_NOHUGEPAGE doesn't split existing THP created in 2.
> 4) dest tries to access 4Kpage2, the address right after 4Kpage1. As khugepaged collapsed it, there won't be a pagefault, and dest will read it as a zero-filled page, even if UFFD handler wanted to give some non-zero data filled page. I think anyone who wrote the uffd handler would want there to be a pagefault and give the non-zero data filled page and would not expect dest to see a zero-filled page.
> 
> What I am trying to say with the above example is:
> - UFFD registeration + MADV_NOHUGEPAGE should be done by all applications before any data in the region is accessed. Using THPs and accessing data before UFFD registeration + MADV_NOHUGEPAGE can lead to unexpected behaviour and is wrong?

The important point here is that you discard (MADV_DONTNEED) any pages 
you want to fault+place later lazily. That must happen after 
MADV_NOHUGEPAGE but before registering+running userfaultfd.

Then you don't care if page faults / kuhgepaged gave you THPs before 
that. In fact, you want to happily use THPs wherever possible and not
disable them unconditionally right from the start.

> - even in the current kernel code in other places like khugepaged, its only checked if uffd is enabled currently. It is not tracked if it was ever enabled on any VMA.

Right, see above, this all works if you MADV_DONTNEED the pages you want 
to get faults to after disabling THPs.


I just added a bunch of quick printfs to QEMU and ran a precopy+postcopy 
live migration. Looks like my assumption was right:

On the destination:

Writing received pages during precopy # ram_load_precopy()
Writing received pages during precopy
Writing received pages during precopy
Writing received pages during precopy 

Writing received pages during precopy
Writing received pages during precopy
Writing received pages during precopy
Writing received pages during precopy
Writing received pages during precopy
Writing received pages during precopy
Writing received pages during precopy
Writing received pages during precopy
Writing received pages during precopy
Writing received pages during precopy
Writing received pages during precopy
Writing received pages during precopy
Writing received pages during precopy
Writing received pages during precopy
Disabling THP: MADV_NOHUGEPAGE # postcopy_ram_prepare_discard()
Discarding pages # loadvm_postcopy_ram_handle_discard()
Discarding pages
Discarding pages
Discarding pages
Discarding pages
Discarding pages
Discarding pages
Registering UFFD # postcopy_ram_incoming_setup()


Let me know if you need more information.

> Thanks for pointing to mm_forbids_zeropage. Incorporating that into the code, and if I am (hopefully :)) right about qemu and kernel above, then I believe the right code should be:

I'm afraid you are not right about the qemu code :)

> 
> 	if (userfaultfd_armed(pvmw->vma) && mm_forbids_zeropage(pvmw->vma->vm_mm))
> 		return false;
> 
> 	if (!userfaultfd_armed(pvmw->vma)) {
> 		pte_clear_not_present_full(pvmw->vma->vm_mm, pvmw->address, pvmw->pte, false);
> 	} else {
> 		newpte = pte_mkspecial(pfn_pte(page_to_pfn(ZERO_PAGE(pvmw->address)),
> 					       pvmw->vma->vm_page_prot));
> 		ptep_clear_flush(pvmw->vma, pvmw->address, pvmw->pte);
> 		set_pte_at(pvmw->vma->vm_mm, pvmw->address, pvmw->pte, newpte);
> 	}
>   
> 
> 
> [1] https://github.com/qemu/qemu/blob/4e56e89d6c81589cc47cf5811f570c67889bd18a/migration/migration.c#L3817
> [2] https://github.com/qemu/qemu/blob/4e56e89d6c81589cc47cf5811f570c67889bd18a/migration/migration.c#L3455
> [3] https://github.com/qemu/qemu/blob/4e56e89d6c81589cc47cf5811f570c67889bd18a/migration/migration.c#L3591
> [4] https://github.com/qemu/qemu/blob/4e56e89d6c81589cc47cf5811f570c67889bd18a/migration/migration.c#L3675
> [5] https://github.com/torvalds/linux/blob/master/mm/khugepaged.c#L1307
> 
> 
>
Usama Arif July 31, 2024, 8:41 p.m. UTC | #9
On 31/07/2024 18:51, David Hildenbrand wrote:
> On 31.07.24 19:01, Usama Arif wrote:
>>
>>
>> On 30/07/2024 21:25, David Hildenbrand wrote:
>>> On 30.07.24 19:22, Usama Arif wrote:
>>>>
>>>>
>>>> On 30/07/2024 17:11, David Hildenbrand wrote:
>>>>> On 30.07.24 17:19, Usama Arif wrote:
>>>>>>
>>>>>>
>>>>>> On 30/07/2024 16:14, Usama Arif wrote:
>>>>>>>
>>>>>>>
>>>>>>> On 30/07/2024 15:35, David Hildenbrand wrote:
>>>>>>>> On 30.07.24 14:45, Usama Arif wrote:
>>>>>>>>> The current upstream default policy for THP is always. However, Meta
>>>>>>>>> uses madvise in production as the current THP=always policy vastly
>>>>>>>>> overprovisions THPs in sparsely accessed memory areas, resulting in
>>>>>>>>> excessive memory pressure and premature OOM killing.
>>>>>>>>> Using madvise + relying on khugepaged has certain drawbacks over
>>>>>>>>> THP=always. Using madvise hints mean THPs aren't "transparent" and
>>>>>>>>> require userspace changes. Waiting for khugepaged to scan memory and
>>>>>>>>> collapse pages into THP can be slow and unpredictable in terms of performance
>>>>>>>>> (i.e. you dont know when the collapse will happen), while production
>>>>>>>>> environments require predictable performance. If there is enough memory
>>>>>>>>> available, its better for both performance and predictability to have
>>>>>>>>> a THP from fault time, i.e. THP=always rather than wait for khugepaged
>>>>>>>>> to collapse it, and deal with sparsely populated THPs when the system is
>>>>>>>>> running out of memory.
>>>>>>>>>
>>>>>>>>> This patch-series is an attempt to mitigate the issue of running out of
>>>>>>>>> memory when THP is always enabled. During runtime whenever a THP is being
>>>>>>>>> faulted in or collapsed by khugepaged, the THP is added to a list.
>>>>>>>>> Whenever memory reclaim happens, the kernel runs the deferred_split
>>>>>>>>> shrinker which goes through the list and checks if the THP was underutilized,
>>>>>>>>> i.e. how many of the base 4K pages of the entire THP were zero-filled.
>>>>>>>>> If this number goes above a certain threshold, the shrinker will attempt
>>>>>>>>> to split that THP. Then at remap time, the pages that were zero-filled are
>>>>>>>>> not remapped, hence saving memory. This method avoids the downside of
>>>>>>>>> wasting memory in areas where THP is sparsely filled when THP is always
>>>>>>>>> enabled, while still providing the upside THPs like reduced TLB misses without
>>>>>>>>> having to use madvise.
>>>>>>>>>
>>>>>>>>> Meta production workloads that were CPU bound (>99% CPU utilzation) were
>>>>>>>>> tested with THP shrinker. The results after 2 hours are as follows:
>>>>>>>>>
>>>>>>>>>                                 | THP=madvise |  THP=always   | THP=always
>>>>>>>>>                                 |             |               | + shrinker series
>>>>>>>>>                                 |             |               | + max_ptes_none=409
>>>>>>>>> -----------------------------------------------------------------------------
>>>>>>>>> Performance improvement     |      -      |    +1.8%      |     +1.7%
>>>>>>>>> (over THP=madvise)          |             |               |
>>>>>>>>> -----------------------------------------------------------------------------
>>>>>>>>> Memory usage                |    54.6G    | 58.8G (+7.7%) |   55.9G (+2.4%)
>>>>>>>>> -----------------------------------------------------------------------------
>>>>>>>>> max_ptes_none=409 means that any THP that has more than 409 out of 512
>>>>>>>>> (80%) zero filled filled pages will be split.
>>>>>>>>>
>>>>>>>>> To test out the patches, the below commands without the shrinker will
>>>>>>>>> invoke OOM killer immediately and kill stress, but will not fail with
>>>>>>>>> the shrinker:
>>>>>>>>>
>>>>>>>>> echo 450 > /sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none
>>>>>>>>> mkdir /sys/fs/cgroup/test
>>>>>>>>> echo $$ > /sys/fs/cgroup/test/cgroup.procs
>>>>>>>>> echo 20M > /sys/fs/cgroup/test/memory.max
>>>>>>>>> echo 0 > /sys/fs/cgroup/test/memory.swap.max
>>>>>>>>> # allocate twice memory.max for each stress worker and touch 40/512 of
>>>>>>>>> # each THP, i.e. vm-stride 50K.
>>>>>>>>> # With the shrinker, max_ptes_none of 470 and below won't invoke OOM
>>>>>>>>> # killer.
>>>>>>>>> # Without the shrinker, OOM killer is invoked immediately irrespective
>>>>>>>>> # of max_ptes_none value and kill stress.
>>>>>>>>> stress --vm 1 --vm-bytes 40M --vm-stride 50K
>>>>>>>>>
>>>>>>>>> Patches 1-2 add back helper functions that were previously removed
>>>>>>>>> to operate on page lists (needed by patch 3).
>>>>>>>>> Patch 3 is an optimization to free zapped tail pages rather than
>>>>>>>>> waiting for page reclaim or migration.
>>>>>>>>> Patch 4 is a prerequisite for THP shrinker to not remap zero-filled
>>>>>>>>> subpages when splitting THP.
>>>>>>>>> Patches 6 adds support for THP shrinker.
>>>>>>>>>
>>>>>>>>> (This patch-series restarts the work on having a THP shrinker in kernel
>>>>>>>>> originally done in
>>>>>>>>> https://lore.kernel.org/all/cover.1667454613.git.alexlzhu@fb.com/.
>>>>>>>>> The THP shrinker in this series is significantly different than the
>>>>>>>>> original one, hence its labelled v1 (although the prerequisite to not
>>>>>>>>> remap clean subpages is the same).)
>>>>>>>>
>>>>>>>> As shared previously, there is one issue with uffd (even when currently not active for a VMA!), where we must not zap present page table entries.
>>>>>>>>
>>>>>>>> Something that is always possible (assuming no GUP pins of course, which) is replacing the zero-filled subpages by shared zeropages.
>>>>>>>>
>>>>>>>> Is that being done in this patch set already, or are we creating pte_none() entries?
>>>>>>>>
>>>>>>>
>>>>>>> I think thats done in Patch 4/6. In function try_to_unmap_unused, we have below which I think does what you are suggesting? i.e. point to shared zeropage and not clear pte for uffd armed vma.
>>>>>>>
>>>>>>>       if (userfaultfd_armed(pvmw->vma)) {
>>>>>>>           newpte = pte_mkspecial(pfn_pte(page_to_pfn(ZERO_PAGE(pvmw->address)),
>>>>>>>                              pvmw->vma->vm_page_prot));
>>>>>>>           ptep_clear_flush(pvmw->vma, pvmw->address, pvmw->pte);
>>>>>>>           set_pte_at(pvmw->vma->vm_mm, pvmw->address, pvmw->pte, newpte);
>>>>>>>       }
>>>>>>
>>>>>>
>>>>>> Ah are you suggesting userfaultfd_armed(pvmw->vma) will evaluate to false even if its uffd? I think something like below would work in that case.
>>>>>
>>>>> I remember one ugly case in QEMU with postcopy live-migration where we must not zap zero-filled pages. I am not 100% regarding THP (if it could be enabled at that point), but imagine the following
>>>>>
>>>>> 1) mmap(), enable THP
>>>>> 2) Migrate a bunch of pages from the source during precopy (writing to
>>>>>      the memory). Might end up creating THPs (during fault/khugepaged)
>>>>> 3) Register UFFD on the VMA
>>>>> 4) Disable new THPs from forming via MADV_NOHUGEPAGE on the VMA
>>>>> 5) Discard any pages that have been re-dirtied or not migrated yet
>>>>> 6) Migrate-on-demand any holes using uffd
>>>>>
>>>>>
>>>>> If we discard zero-filled pages between 2) and 3) we might get wrong uffd notifications in 6 for pages that have already been migrated).
>>>>>
>>>>> I'll have to check if that actually happens in that sequence in QEMU: if QEMU would disable THP right before 2) we would be safe. But I recall that it is not the case :/
>>>>>
>>>>>
>>>>
>>>> Thanks for the example!
>>>>
>>>> Just to understand the issue better, as I am not very familiar with live-migration code, the problem is only for zero-filled pages that were migrated, right? If a THP is created and a subpage of it was a zero-page that was migrated and its split before VMA is armed with uffd, userfaultfd_armed(pvmw->vma) will return false when splitting and it will become pte_none. And afterwards when the destination faults on it, uffd will see that its pte_clear and will request the zero-page back from source. Uffd will then have to get the page again from source.
>>>
>>> Something like that, but I recall that if it detects that it already migrated the page stuff will go wrong.
>>>
>>> IIRC QEMU maintains receive bitmaps about which pages it already received+placed. Staring at migrate_send_rp_req_pages(), QEMU ignores the uffd notification if it finds that the page was already received and would not ask the source again.
>>>
>>> So if we turn some PTEs that map zeroed-pages into pte-none we could run into trouble, because we will never try requesting/placing that page again and our VM will just be stuck forever.
>>>
>>> I wonder if other uffd users could similarly be affected. But maybe they don't place pages into the VMA before registering uffd.
>>>
>>> I'll try to double-check when QEMU would disable THP. But it could also be that that behavior changed between QEMU versions.
>>>
>>>>
>>>> If I understand the example correctly, the below diff over patch 6 should be good? i.e. just point to the empty_zero_page instead of doing pte_clear. This should still use the same amount of memory, although ptep_clear_flush means it might be slighly more expensive.
>>>
>>> There are rare cases where we must not use the shared zeropage (mm_forbids_zeropage()), that must be handled.
>>>
>>> I assume we could optimize here if uffd is not configured in. Further, what QEMU does is sense right at the beginning by temporarily registering uffd on some other VMA if it is even supported. That could be used as an indication ("ever used uffd -> don't turn PTEs none"). But again, no idea what other uffd users might be relying on :/
>>>
>>> In I wonder if some applications could rely on anon memory not suddenly "vanishing" form the PTEs (for example relying on pagemap like tools/testing/selftests/mm/cow.c) would. I don't think a lot of applications would do that, though.
>>>
>>
>>
>> I had a deeper look at how QEMU handles migration and at how kernel handles THPs in other places with respect to uffd. I hope I understood the migration code correctly :)
>>
>> QEMU:
>> There are 2 types of "migrations" [1], migration_thread and bg_migration_thread
>> - migration_thread supports postcopy live migration, but doesn't use uffd. So postcopy live migration doesn't use uffd as far as I can tell looking at the function [2].
>> - bg_migration_thread [3]: this isn't really live migration, but background snapshot. There is no postcopy and it works similar to pre-copy. From what I understand, uffd is registered before any pages are migrated [4].
>>
> 
> Ignore the second. What QEMU ends up using is precopy + postcopy. You can start precopy migration and decide at some point that you want to switch to postcopy (this is what I trigger below, let me know if you want a simple way to reproduce it).
> 
>> So the way these 2 threads work in qemu is: its either postcopy without uffd, or precopy-style snapshot with uffd registered at the start, then the current patch in this series is good, i.e. only use zeropage if userfaultfd_armed(pvmw->vma), otherwise pte_clear. I can't see postcopy with uffd anywhere in qemu which doesn all the steps 1-6 that you mentioned above, but I might not be looking at the right place?
> 
> "I can't see postcopy with uffd anywhere" -- most of it lives in migration/postcopy-ram.c.
> 
> See postcopy_ram_incoming_setup() where most of the UFFD setup logic happens.
> 

Thanks for pointing to this, I did not look at this before :( postcopy_ram_incoming_setup clears it up.

>>
>> Kernel:
>>  From a kernel perspective, if we look at khugepaged [5], it just takes into account if the vma is currently registered as uffd. So maybe this scenario can happen?
>> 1) THP is enabled on VMA, UFFD is not yet registered.
>> 2) dest accesses a 4K page (lets call it 4Kpage1), khugepaged collapses that page into 2M THP as the following 511 4K pages were pte_none [5], as VMA is not uffd armed yet.
>> 3) UFFD is registered + MADV_NOHUGEPAGE is set. No further collapse will happen, but the THPs that were created in step 2 will remain. MADV_NOHUGEPAGE doesn't split existing THP created in 2.
>> 4) dest tries to access 4Kpage2, the address right after 4Kpage1. As khugepaged collapsed it, there won't be a pagefault, and dest will read it as a zero-filled page, even if UFFD handler wanted to give some non-zero data filled page. I think anyone who wrote the uffd handler would want there to be a pagefault and give the non-zero data filled page and would not expect dest to see a zero-filled page.
>>
>> What I am trying to say with the above example is:
>> - UFFD registeration + MADV_NOHUGEPAGE should be done by all applications before any data in the region is accessed. Using THPs and accessing data before UFFD registeration + MADV_NOHUGEPAGE can lead to unexpected behaviour and is wrong?
> 
> The important point here is that you discard (MADV_DONTNEED) any pages you want to fault+place later lazily. That must happen after MADV_NOHUGEPAGE but before registering+running userfaultfd.
> 
> Then you don't care if page faults / kuhgepaged gave you THPs before that. In fact, you want to happily use THPs wherever possible and not
> disable them unconditionally right from the start.
> 
>> - even in the current kernel code in other places like khugepaged, its only checked if uffd is enabled currently. It is not tracked if it was ever enabled on any VMA.
> 
> Right, see above, this all works if you MADV_DONTNEED the pages you want to get faults to after disabling THPs.
> 
> 
> I just added a bunch of quick printfs to QEMU and ran a precopy+postcopy live migration. Looks like my assumption was right:
> 
> On the destination:
> 
> Writing received pages during precopy # ram_load_precopy()
> Writing received pages during precopy
> Writing received pages during precopy
> Writing received pages during precopy
> Writing received pages during precopy
> Writing received pages during precopy
> Writing received pages during precopy
> Writing received pages during precopy
> Writing received pages during precopy
> Writing received pages during precopy
> Writing received pages during precopy
> Writing received pages during precopy
> Writing received pages during precopy
> Writing received pages during precopy
> Writing received pages during precopy
> Writing received pages during precopy
> Writing received pages during precopy
> Writing received pages during precopy
> Disabling THP: MADV_NOHUGEPAGE # postcopy_ram_prepare_discard()
> Discarding pages # loadvm_postcopy_ram_handle_discard()
> Discarding pages
> Discarding pages
> Discarding pages
> Discarding pages
> Discarding pages
> Discarding pages
> Registering UFFD # postcopy_ram_incoming_setup()
> 

Thanks for this, yes it makes sense after you mentioned postcopy_ram_incoming_setup. 
postcopy_ram_incoming_setup happens in the Listen phase, which is after the discard phase, so I was able to follow in code in qemu the same sequence of events that the above prints show.
> 
> Let me know if you need more information.
> 
>> Thanks for pointing to mm_forbids_zeropage. Incorporating that into the code, and if I am (hopefully :)) right about qemu and kernel above, then I believe the right code should be:
> 
> I'm afraid you are not right about the qemu code :)
> 

Yes, and also didn't consider MADV_DONTNEED! Thanks for explaining both of these things clearly. Its clear that pte_clear won't work in this case. 

We don't need to clear_pte, just use zero_page for all cases. The original series from Alex did tlb flush, but looking further at the code, thats not needed. try_to_migrate() flushes tlb and installs migration entries which are not ‘present’ so will never be tlb cached. remove_migration_ptes() restores page pointers so tlb flushing is not needed. When using zeropage, we don't need make a distinction if uffd is used or not. i.e. we can just do below:

	if (contains_data || mm_forbids_zeropage(pvmw->vma->vm_mm))
		return false;

	newpte = pte_mkspecial(pfn_pte(page_to_pfn(ZERO_PAGE(pvmw->address)),
					pvmw->vma->vm_page_prot));

	set_pte_at(pvmw->vma->vm_mm, pvmw->address, pvmw->pte, newpte);
Yu Zhao Aug. 1, 2024, 6:09 a.m. UTC | #10
On Tue, Jul 30, 2024 at 6:54 AM Usama Arif <usamaarif642@gmail.com> wrote:
>
> The current upstream default policy for THP is always. However, Meta
> uses madvise in production as the current THP=always policy vastly
> overprovisions THPs in sparsely accessed memory areas, resulting in
> excessive memory pressure and premature OOM killing.
> Using madvise + relying on khugepaged has certain drawbacks over
> THP=always. Using madvise hints mean THPs aren't "transparent" and
> require userspace changes. Waiting for khugepaged to scan memory and
> collapse pages into THP can be slow and unpredictable in terms of performance
> (i.e. you dont know when the collapse will happen), while production
> environments require predictable performance. If there is enough memory
> available, its better for both performance and predictability to have
> a THP from fault time, i.e. THP=always rather than wait for khugepaged
> to collapse it, and deal with sparsely populated THPs when the system is
> running out of memory.
>
> This patch-series is an attempt to mitigate the issue of running out of
> memory when THP is always enabled. During runtime whenever a THP is being
> faulted in or collapsed by khugepaged, the THP is added to a list.
> Whenever memory reclaim happens, the kernel runs the deferred_split
> shrinker which goes through the list and checks if the THP was underutilized,
> i.e. how many of the base 4K pages of the entire THP were zero-filled.
> If this number goes above a certain threshold, the shrinker will attempt
> to split that THP. Then at remap time, the pages that were zero-filled are
> not remapped, hence saving memory. This method avoids the downside of
> wasting memory in areas where THP is sparsely filled when THP is always
> enabled, while still providing the upside THPs like reduced TLB misses without
> having to use madvise.
>
> Meta production workloads that were CPU bound (>99% CPU utilzation) were
> tested with THP shrinker. The results after 2 hours are as follows:
>
>                             | THP=madvise |  THP=always   | THP=always
>                             |             |               | + shrinker series
>                             |             |               | + max_ptes_none=409
> -----------------------------------------------------------------------------
> Performance improvement     |      -      |    +1.8%      |     +1.7%
> (over THP=madvise)          |             |               |
> -----------------------------------------------------------------------------
> Memory usage                |    54.6G    | 58.8G (+7.7%) |   55.9G (+2.4%)
> -----------------------------------------------------------------------------
> max_ptes_none=409 means that any THP that has more than 409 out of 512
> (80%) zero filled filled pages will be split.
>
> To test out the patches, the below commands without the shrinker will
> invoke OOM killer immediately and kill stress, but will not fail with
> the shrinker:
>
> echo 450 > /sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none
> mkdir /sys/fs/cgroup/test
> echo $$ > /sys/fs/cgroup/test/cgroup.procs
> echo 20M > /sys/fs/cgroup/test/memory.max
> echo 0 > /sys/fs/cgroup/test/memory.swap.max
> # allocate twice memory.max for each stress worker and touch 40/512 of
> # each THP, i.e. vm-stride 50K.
> # With the shrinker, max_ptes_none of 470 and below won't invoke OOM
> # killer.
> # Without the shrinker, OOM killer is invoked immediately irrespective
> # of max_ptes_none value and kill stress.
> stress --vm 1 --vm-bytes 40M --vm-stride 50K
>
> Patches 1-2 add back helper functions that were previously removed
> to operate on page lists (needed by patch 3).
> Patch 3 is an optimization to free zapped tail pages rather than
> waiting for page reclaim or migration.
> Patch 4 is a prerequisite for THP shrinker to not remap zero-filled
> subpages when splitting THP.
> Patches 6 adds support for THP shrinker.
>
> (This patch-series restarts the work on having a THP shrinker in kernel
> originally done in
> https://lore.kernel.org/all/cover.1667454613.git.alexlzhu@fb.com/.
> The THP shrinker in this series is significantly different than the
> original one, hence its labelled v1 (although the prerequisite to not
> remap clean subpages is the same).)
>
> Alexander Zhu (1):
>   mm: add selftests to split_huge_page() to verify unmap/zap of zero
>     pages
>
> Usama Arif (3):
>   Revert "memcg: remove mem_cgroup_uncharge_list()"
>   Revert "mm: remove free_unref_page_list()"
>   mm: split underutilized THPs
>
> Yu Zhao (2):
>   mm: free zapped tail pages when splitting isolated thp
>   mm: don't remap unused subpages when splitting isolated thp

 I would recommend shatter [1] instead of splitting so that
1) whoever underutilized their THPs get punished for the overhead;
2) underutilized THPs are kept intact and can be reused by others.

[1] https://lore.kernel.org/20240229183436.4110845-3-yuzhao@google.com/
David Hildenbrand Aug. 1, 2024, 6:36 a.m. UTC | #11
>> I just added a bunch of quick printfs to QEMU and ran a precopy+postcopy live migration. Looks like my assumption was right:
>>
>> On the destination:
>>
>> Writing received pages during precopy # ram_load_precopy()
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Disabling THP: MADV_NOHUGEPAGE # postcopy_ram_prepare_discard()
>> Discarding pages # loadvm_postcopy_ram_handle_discard()
>> Discarding pages
>> Discarding pages
>> Discarding pages
>> Discarding pages
>> Discarding pages
>> Discarding pages
>> Registering UFFD # postcopy_ram_incoming_setup()
>>
> 
> Thanks for this, yes it makes sense after you mentioned postcopy_ram_incoming_setup.
> postcopy_ram_incoming_setup happens in the Listen phase, which is after the discard phase, so I was able to follow in code in qemu the same sequence of events that the above prints show.


I just added another printf to postcopy_ram_supported_by_host(), where 
we temporarily do a UFFDIO_REGISTER on some test area.

Sensing UFFD support # postcopy_ram_supported_by_host()
Sensing UFFD support
Writing received pages during precopy # ram_load_precopy()
Writing received pages during precopy
Writing received pages during precopy
Writing received pages during precopy
Writing received pages during precopy
Writing received pages during precopy
Writing received pages during precopy
Writing received pages during precopy
Writing received pages during precopy
Writing received pages during precopy
Writing received pages during precopy
Writing received pages during precopy
Writing received pages during precopy
Writing received pages during precopy
Writing received pages during precopy
Writing received pages during precopy
Writing received pages during precopy
Writing received pages during precopy
Writing received pages during precopy
Writing received pages during precopy
Writing received pages during precopy
Writing received pages during precopy
Disabling THP: MADV_NOHUGEPAGE # postcopy_ram_prepare_discard()
Discarding pages # loadvm_postcopy_ram_handle_discard()
Discarding pages
Discarding pages
Discarding pages
Discarding pages
Discarding pages
Discarding pages
Discarding pages
Discarding pages
Discarding pages
Discarding pages
Discarding pages
Discarding pages
Discarding pages
Discarding pages
Discarding pages
Registering UFFD # postcopy_ram_incoming_setup()

We could think about using this "ever user uffd" to avoid the shared 
zeropage in most processes.

Of course, there might be other applications where that wouldn't work, 
but I think this behavior (write to area before enabling uffd) might be 
fairly QEMU specific already.

Avoiding the shared zeropage has the benefit that a later write fault 
won't have to do a TLB flush and can simply install a fresh anon page.

>>
>> Let me know if you need more information.
>>
>>> Thanks for pointing to mm_forbids_zeropage. Incorporating that into the code, and if I am (hopefully :)) right about qemu and kernel above, then I believe the right code should be:
>>
>> I'm afraid you are not right about the qemu code :)
>>
> 
> Yes, and also didn't consider MADV_DONTNEED! Thanks for explaining both of these things clearly. Its clear that pte_clear won't work in this case.
> 
> We don't need to clear_pte, just use zero_page for all cases. The original series from Alex did tlb flush, but looking further at the code, thats not needed. try_to_migrate() flushes tlb and installs migration entries which are not ‘present’ so will never be tlb cached. remove_migration_ptes() restores page pointers so tlb flushing is not needed. When using zeropage, we don't need make a distinction if uffd is used or not. i.e. we can just do below:
> 
> 	if (contains_data || mm_forbids_zeropage(pvmw->vma->vm_mm))

It's worth noting that on s390x, MMs that forbid the zeropage also have 
THPs disabled. So we shouldn't really run into that that often (of 
course, it's subject to change in the future, so we better have this 
check here).

> 		return false;
> 
> 	newpte = pte_mkspecial(pfn_pte(page_to_pfn(ZERO_PAGE(pvmw->address)),
> 					pvmw->vma->vm_page_prot));
> 
> 	set_pte_at(pvmw->vma->vm_mm, pvmw->address, pvmw->pte, newpte);

We're replacing a present page by another present page without doing a 
TLB flush in between. I *think* this should be fine because the new 
present page is R/O and cannot possibly be written to.
David Hildenbrand Aug. 1, 2024, 3:47 p.m. UTC | #12
On 01.08.24 08:09, Yu Zhao wrote:
> On Tue, Jul 30, 2024 at 6:54 AM Usama Arif <usamaarif642@gmail.com> wrote:
>>
>> The current upstream default policy for THP is always. However, Meta
>> uses madvise in production as the current THP=always policy vastly
>> overprovisions THPs in sparsely accessed memory areas, resulting in
>> excessive memory pressure and premature OOM killing.
>> Using madvise + relying on khugepaged has certain drawbacks over
>> THP=always. Using madvise hints mean THPs aren't "transparent" and
>> require userspace changes. Waiting for khugepaged to scan memory and
>> collapse pages into THP can be slow and unpredictable in terms of performance
>> (i.e. you dont know when the collapse will happen), while production
>> environments require predictable performance. If there is enough memory
>> available, its better for both performance and predictability to have
>> a THP from fault time, i.e. THP=always rather than wait for khugepaged
>> to collapse it, and deal with sparsely populated THPs when the system is
>> running out of memory.
>>
>> This patch-series is an attempt to mitigate the issue of running out of
>> memory when THP is always enabled. During runtime whenever a THP is being
>> faulted in or collapsed by khugepaged, the THP is added to a list.
>> Whenever memory reclaim happens, the kernel runs the deferred_split
>> shrinker which goes through the list and checks if the THP was underutilized,
>> i.e. how many of the base 4K pages of the entire THP were zero-filled.
>> If this number goes above a certain threshold, the shrinker will attempt
>> to split that THP. Then at remap time, the pages that were zero-filled are
>> not remapped, hence saving memory. This method avoids the downside of
>> wasting memory in areas where THP is sparsely filled when THP is always
>> enabled, while still providing the upside THPs like reduced TLB misses without
>> having to use madvise.
>>
>> Meta production workloads that were CPU bound (>99% CPU utilzation) were
>> tested with THP shrinker. The results after 2 hours are as follows:
>>
>>                              | THP=madvise |  THP=always   | THP=always
>>                              |             |               | + shrinker series
>>                              |             |               | + max_ptes_none=409
>> -----------------------------------------------------------------------------
>> Performance improvement     |      -      |    +1.8%      |     +1.7%
>> (over THP=madvise)          |             |               |
>> -----------------------------------------------------------------------------
>> Memory usage                |    54.6G    | 58.8G (+7.7%) |   55.9G (+2.4%)
>> -----------------------------------------------------------------------------
>> max_ptes_none=409 means that any THP that has more than 409 out of 512
>> (80%) zero filled filled pages will be split.
>>
>> To test out the patches, the below commands without the shrinker will
>> invoke OOM killer immediately and kill stress, but will not fail with
>> the shrinker:
>>
>> echo 450 > /sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none
>> mkdir /sys/fs/cgroup/test
>> echo $$ > /sys/fs/cgroup/test/cgroup.procs
>> echo 20M > /sys/fs/cgroup/test/memory.max
>> echo 0 > /sys/fs/cgroup/test/memory.swap.max
>> # allocate twice memory.max for each stress worker and touch 40/512 of
>> # each THP, i.e. vm-stride 50K.
>> # With the shrinker, max_ptes_none of 470 and below won't invoke OOM
>> # killer.
>> # Without the shrinker, OOM killer is invoked immediately irrespective
>> # of max_ptes_none value and kill stress.
>> stress --vm 1 --vm-bytes 40M --vm-stride 50K
>>
>> Patches 1-2 add back helper functions that were previously removed
>> to operate on page lists (needed by patch 3).
>> Patch 3 is an optimization to free zapped tail pages rather than
>> waiting for page reclaim or migration.
>> Patch 4 is a prerequisite for THP shrinker to not remap zero-filled
>> subpages when splitting THP.
>> Patches 6 adds support for THP shrinker.
>>
>> (This patch-series restarts the work on having a THP shrinker in kernel
>> originally done in
>> https://lore.kernel.org/all/cover.1667454613.git.alexlzhu@fb.com/.
>> The THP shrinker in this series is significantly different than the
>> original one, hence its labelled v1 (although the prerequisite to not
>> remap clean subpages is the same).)
>>
>> Alexander Zhu (1):
>>    mm: add selftests to split_huge_page() to verify unmap/zap of zero
>>      pages
>>
>> Usama Arif (3):
>>    Revert "memcg: remove mem_cgroup_uncharge_list()"
>>    Revert "mm: remove free_unref_page_list()"
>>    mm: split underutilized THPs
>>
>> Yu Zhao (2):
>>    mm: free zapped tail pages when splitting isolated thp
>>    mm: don't remap unused subpages when splitting isolated thp
> 
>   I would recommend shatter [1] instead of splitting so that
> 1) whoever underutilized their THPs get punished for the overhead;
> 2) underutilized THPs are kept intact and can be reused by others.
> 
> [1] https://lore.kernel.org/20240229183436.4110845-3-yuzhao@google.com/
> 

Do you have any plans to upstream the shattering also during "ordinary" 
deferred splitting?
Usama Arif Aug. 1, 2024, 4:22 p.m. UTC | #13
On 01/08/2024 07:09, Yu Zhao wrote:
> On Tue, Jul 30, 2024 at 6:54 AM Usama Arif <usamaarif642@gmail.com> wrote:
>>
>> The current upstream default policy for THP is always. However, Meta
>> uses madvise in production as the current THP=always policy vastly
>> overprovisions THPs in sparsely accessed memory areas, resulting in
>> excessive memory pressure and premature OOM killing.
>> Using madvise + relying on khugepaged has certain drawbacks over
>> THP=always. Using madvise hints mean THPs aren't "transparent" and
>> require userspace changes. Waiting for khugepaged to scan memory and
>> collapse pages into THP can be slow and unpredictable in terms of performance
>> (i.e. you dont know when the collapse will happen), while production
>> environments require predictable performance. If there is enough memory
>> available, its better for both performance and predictability to have
>> a THP from fault time, i.e. THP=always rather than wait for khugepaged
>> to collapse it, and deal with sparsely populated THPs when the system is
>> running out of memory.
>>
>> This patch-series is an attempt to mitigate the issue of running out of
>> memory when THP is always enabled. During runtime whenever a THP is being
>> faulted in or collapsed by khugepaged, the THP is added to a list.
>> Whenever memory reclaim happens, the kernel runs the deferred_split
>> shrinker which goes through the list and checks if the THP was underutilized,
>> i.e. how many of the base 4K pages of the entire THP were zero-filled.
>> If this number goes above a certain threshold, the shrinker will attempt
>> to split that THP. Then at remap time, the pages that were zero-filled are
>> not remapped, hence saving memory. This method avoids the downside of
>> wasting memory in areas where THP is sparsely filled when THP is always
>> enabled, while still providing the upside THPs like reduced TLB misses without
>> having to use madvise.
>>
>> Meta production workloads that were CPU bound (>99% CPU utilzation) were
>> tested with THP shrinker. The results after 2 hours are as follows:
>>
>>                             | THP=madvise |  THP=always   | THP=always
>>                             |             |               | + shrinker series
>>                             |             |               | + max_ptes_none=409
>> -----------------------------------------------------------------------------
>> Performance improvement     |      -      |    +1.8%      |     +1.7%
>> (over THP=madvise)          |             |               |
>> -----------------------------------------------------------------------------
>> Memory usage                |    54.6G    | 58.8G (+7.7%) |   55.9G (+2.4%)
>> -----------------------------------------------------------------------------
>> max_ptes_none=409 means that any THP that has more than 409 out of 512
>> (80%) zero filled filled pages will be split.
>>
>> To test out the patches, the below commands without the shrinker will
>> invoke OOM killer immediately and kill stress, but will not fail with
>> the shrinker:
>>
>> echo 450 > /sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none
>> mkdir /sys/fs/cgroup/test
>> echo $$ > /sys/fs/cgroup/test/cgroup.procs
>> echo 20M > /sys/fs/cgroup/test/memory.max
>> echo 0 > /sys/fs/cgroup/test/memory.swap.max
>> # allocate twice memory.max for each stress worker and touch 40/512 of
>> # each THP, i.e. vm-stride 50K.
>> # With the shrinker, max_ptes_none of 470 and below won't invoke OOM
>> # killer.
>> # Without the shrinker, OOM killer is invoked immediately irrespective
>> # of max_ptes_none value and kill stress.
>> stress --vm 1 --vm-bytes 40M --vm-stride 50K
>>
>> Patches 1-2 add back helper functions that were previously removed
>> to operate on page lists (needed by patch 3).
>> Patch 3 is an optimization to free zapped tail pages rather than
>> waiting for page reclaim or migration.
>> Patch 4 is a prerequisite for THP shrinker to not remap zero-filled
>> subpages when splitting THP.
>> Patches 6 adds support for THP shrinker.
>>
>> (This patch-series restarts the work on having a THP shrinker in kernel
>> originally done in
>> https://lore.kernel.org/all/cover.1667454613.git.alexlzhu@fb.com/.
>> The THP shrinker in this series is significantly different than the
>> original one, hence its labelled v1 (although the prerequisite to not
>> remap clean subpages is the same).)
>>
>> Alexander Zhu (1):
>>   mm: add selftests to split_huge_page() to verify unmap/zap of zero
>>     pages
>>
>> Usama Arif (3):
>>   Revert "memcg: remove mem_cgroup_uncharge_list()"
>>   Revert "mm: remove free_unref_page_list()"
>>   mm: split underutilized THPs
>>
>> Yu Zhao (2):
>>   mm: free zapped tail pages when splitting isolated thp
>>   mm: don't remap unused subpages when splitting isolated thp
> 
>  I would recommend shatter [1] instead of splitting so that
> 1) whoever underutilized their THPs get punished for the overhead;
> 2) underutilized THPs are kept intact and can be reused by others.
> 
> [1] https://lore.kernel.org/20240229183436.4110845-3-yuzhao@google.com/

The objective of this series is to reduce memory usage, while trying to keep the performance benefits you get of using THP=always. Punishing any applications performance is the opposite of what I am trying to do here.
For e.g. if there is only one main application running in production, and its using majority of the THPs, then reducing its performance doesn't make sense.

Also, just going through the commit, and found the line "The advantage of shattering is that it keeps the original THP intact" a bit confusing. I am guessing the THP is freed? i.e. if a 2M THP has 10 non-zero filled base pages and the rest are zero-filled, then after shattering we will have 10*4K memory and not 2M+10*4K? Is it the case the THP is reused at next fault?
David Hildenbrand Aug. 1, 2024, 4:27 p.m. UTC | #14
On 01.08.24 18:22, Usama Arif wrote:
> 
> 
> On 01/08/2024 07:09, Yu Zhao wrote:
>> On Tue, Jul 30, 2024 at 6:54 AM Usama Arif <usamaarif642@gmail.com> wrote:
>>>
>>> The current upstream default policy for THP is always. However, Meta
>>> uses madvise in production as the current THP=always policy vastly
>>> overprovisions THPs in sparsely accessed memory areas, resulting in
>>> excessive memory pressure and premature OOM killing.
>>> Using madvise + relying on khugepaged has certain drawbacks over
>>> THP=always. Using madvise hints mean THPs aren't "transparent" and
>>> require userspace changes. Waiting for khugepaged to scan memory and
>>> collapse pages into THP can be slow and unpredictable in terms of performance
>>> (i.e. you dont know when the collapse will happen), while production
>>> environments require predictable performance. If there is enough memory
>>> available, its better for both performance and predictability to have
>>> a THP from fault time, i.e. THP=always rather than wait for khugepaged
>>> to collapse it, and deal with sparsely populated THPs when the system is
>>> running out of memory.
>>>
>>> This patch-series is an attempt to mitigate the issue of running out of
>>> memory when THP is always enabled. During runtime whenever a THP is being
>>> faulted in or collapsed by khugepaged, the THP is added to a list.
>>> Whenever memory reclaim happens, the kernel runs the deferred_split
>>> shrinker which goes through the list and checks if the THP was underutilized,
>>> i.e. how many of the base 4K pages of the entire THP were zero-filled.
>>> If this number goes above a certain threshold, the shrinker will attempt
>>> to split that THP. Then at remap time, the pages that were zero-filled are
>>> not remapped, hence saving memory. This method avoids the downside of
>>> wasting memory in areas where THP is sparsely filled when THP is always
>>> enabled, while still providing the upside THPs like reduced TLB misses without
>>> having to use madvise.
>>>
>>> Meta production workloads that were CPU bound (>99% CPU utilzation) were
>>> tested with THP shrinker. The results after 2 hours are as follows:
>>>
>>>                              | THP=madvise |  THP=always   | THP=always
>>>                              |             |               | + shrinker series
>>>                              |             |               | + max_ptes_none=409
>>> -----------------------------------------------------------------------------
>>> Performance improvement     |      -      |    +1.8%      |     +1.7%
>>> (over THP=madvise)          |             |               |
>>> -----------------------------------------------------------------------------
>>> Memory usage                |    54.6G    | 58.8G (+7.7%) |   55.9G (+2.4%)
>>> -----------------------------------------------------------------------------
>>> max_ptes_none=409 means that any THP that has more than 409 out of 512
>>> (80%) zero filled filled pages will be split.
>>>
>>> To test out the patches, the below commands without the shrinker will
>>> invoke OOM killer immediately and kill stress, but will not fail with
>>> the shrinker:
>>>
>>> echo 450 > /sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none
>>> mkdir /sys/fs/cgroup/test
>>> echo $$ > /sys/fs/cgroup/test/cgroup.procs
>>> echo 20M > /sys/fs/cgroup/test/memory.max
>>> echo 0 > /sys/fs/cgroup/test/memory.swap.max
>>> # allocate twice memory.max for each stress worker and touch 40/512 of
>>> # each THP, i.e. vm-stride 50K.
>>> # With the shrinker, max_ptes_none of 470 and below won't invoke OOM
>>> # killer.
>>> # Without the shrinker, OOM killer is invoked immediately irrespective
>>> # of max_ptes_none value and kill stress.
>>> stress --vm 1 --vm-bytes 40M --vm-stride 50K
>>>
>>> Patches 1-2 add back helper functions that were previously removed
>>> to operate on page lists (needed by patch 3).
>>> Patch 3 is an optimization to free zapped tail pages rather than
>>> waiting for page reclaim or migration.
>>> Patch 4 is a prerequisite for THP shrinker to not remap zero-filled
>>> subpages when splitting THP.
>>> Patches 6 adds support for THP shrinker.
>>>
>>> (This patch-series restarts the work on having a THP shrinker in kernel
>>> originally done in
>>> https://lore.kernel.org/all/cover.1667454613.git.alexlzhu@fb.com/.
>>> The THP shrinker in this series is significantly different than the
>>> original one, hence its labelled v1 (although the prerequisite to not
>>> remap clean subpages is the same).)
>>>
>>> Alexander Zhu (1):
>>>    mm: add selftests to split_huge_page() to verify unmap/zap of zero
>>>      pages
>>>
>>> Usama Arif (3):
>>>    Revert "memcg: remove mem_cgroup_uncharge_list()"
>>>    Revert "mm: remove free_unref_page_list()"
>>>    mm: split underutilized THPs
>>>
>>> Yu Zhao (2):
>>>    mm: free zapped tail pages when splitting isolated thp
>>>    mm: don't remap unused subpages when splitting isolated thp
>>
>>   I would recommend shatter [1] instead of splitting so that
>> 1) whoever underutilized their THPs get punished for the overhead;
>> 2) underutilized THPs are kept intact and can be reused by others.
>>
>> [1] https://lore.kernel.org/20240229183436.4110845-3-yuzhao@google.com/
> 
> The objective of this series is to reduce memory usage, while trying to keep the performance benefits you get of using THP=always. Punishing any applications performance is the opposite of what I am trying to do here.
> For e.g. if there is only one main application running in production, and its using majority of the THPs, then reducing its performance doesn't make sense.
> 

I'm not sure if there would really be a performance degradation 
regarding the THP, after all we zap PTEs either way.

Shattering will take longer because real migration is involved IIUC.

> Also, just going through the commit, and found the line "The advantage of shattering is that it keeps the original THP intact" a bit confusing. I am guessing the THP is freed? i.e. if a 2M THP has 10 non-zero filled base pages and the rest are zero-filled, then after shattering we will have 10*4K memory and not 2M+10*4K? Is it the case the THP is reused at next fault?

The idea is (as I understand it) to free the full THP abck to the buddy, 
replacing the individual pieces that are kept to freshly allocated 
order-0 pages from the buddy.
Usama Arif Aug. 4, 2024, 7:10 p.m. UTC | #15
On 01/08/2024 17:27, David Hildenbrand wrote:
> On 01.08.24 18:22, Usama Arif wrote:
>>
>>
>> On 01/08/2024 07:09, Yu Zhao wrote:
>>> On Tue, Jul 30, 2024 at 6:54 AM Usama Arif <usamaarif642@gmail.com> wrote:
>>>>
>>>> The current upstream default policy for THP is always. However, Meta
>>>> uses madvise in production as the current THP=always policy vastly
>>>> overprovisions THPs in sparsely accessed memory areas, resulting in
>>>> excessive memory pressure and premature OOM killing.
>>>> Using madvise + relying on khugepaged has certain drawbacks over
>>>> THP=always. Using madvise hints mean THPs aren't "transparent" and
>>>> require userspace changes. Waiting for khugepaged to scan memory and
>>>> collapse pages into THP can be slow and unpredictable in terms of performance
>>>> (i.e. you dont know when the collapse will happen), while production
>>>> environments require predictable performance. If there is enough memory
>>>> available, its better for both performance and predictability to have
>>>> a THP from fault time, i.e. THP=always rather than wait for khugepaged
>>>> to collapse it, and deal with sparsely populated THPs when the system is
>>>> running out of memory.
>>>>
>>>> This patch-series is an attempt to mitigate the issue of running out of
>>>> memory when THP is always enabled. During runtime whenever a THP is being
>>>> faulted in or collapsed by khugepaged, the THP is added to a list.
>>>> Whenever memory reclaim happens, the kernel runs the deferred_split
>>>> shrinker which goes through the list and checks if the THP was underutilized,
>>>> i.e. how many of the base 4K pages of the entire THP were zero-filled.
>>>> If this number goes above a certain threshold, the shrinker will attempt
>>>> to split that THP. Then at remap time, the pages that were zero-filled are
>>>> not remapped, hence saving memory. This method avoids the downside of
>>>> wasting memory in areas where THP is sparsely filled when THP is always
>>>> enabled, while still providing the upside THPs like reduced TLB misses without
>>>> having to use madvise.
>>>>
>>>> Meta production workloads that were CPU bound (>99% CPU utilzation) were
>>>> tested with THP shrinker. The results after 2 hours are as follows:
>>>>
>>>>                              | THP=madvise |  THP=always   | THP=always
>>>>                              |             |               | + shrinker series
>>>>                              |             |               | + max_ptes_none=409
>>>> -----------------------------------------------------------------------------
>>>> Performance improvement     |      -      |    +1.8%      |     +1.7%
>>>> (over THP=madvise)          |             |               |
>>>> -----------------------------------------------------------------------------
>>>> Memory usage                |    54.6G    | 58.8G (+7.7%) |   55.9G (+2.4%)
>>>> -----------------------------------------------------------------------------
>>>> max_ptes_none=409 means that any THP that has more than 409 out of 512
>>>> (80%) zero filled filled pages will be split.
>>>>
>>>> To test out the patches, the below commands without the shrinker will
>>>> invoke OOM killer immediately and kill stress, but will not fail with
>>>> the shrinker:
>>>>
>>>> echo 450 > /sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none
>>>> mkdir /sys/fs/cgroup/test
>>>> echo $$ > /sys/fs/cgroup/test/cgroup.procs
>>>> echo 20M > /sys/fs/cgroup/test/memory.max
>>>> echo 0 > /sys/fs/cgroup/test/memory.swap.max
>>>> # allocate twice memory.max for each stress worker and touch 40/512 of
>>>> # each THP, i.e. vm-stride 50K.
>>>> # With the shrinker, max_ptes_none of 470 and below won't invoke OOM
>>>> # killer.
>>>> # Without the shrinker, OOM killer is invoked immediately irrespective
>>>> # of max_ptes_none value and kill stress.
>>>> stress --vm 1 --vm-bytes 40M --vm-stride 50K
>>>>
>>>> Patches 1-2 add back helper functions that were previously removed
>>>> to operate on page lists (needed by patch 3).
>>>> Patch 3 is an optimization to free zapped tail pages rather than
>>>> waiting for page reclaim or migration.
>>>> Patch 4 is a prerequisite for THP shrinker to not remap zero-filled
>>>> subpages when splitting THP.
>>>> Patches 6 adds support for THP shrinker.
>>>>
>>>> (This patch-series restarts the work on having a THP shrinker in kernel
>>>> originally done in
>>>> https://lore.kernel.org/all/cover.1667454613.git.alexlzhu@fb.com/.
>>>> The THP shrinker in this series is significantly different than the
>>>> original one, hence its labelled v1 (although the prerequisite to not
>>>> remap clean subpages is the same).)
>>>>
>>>> Alexander Zhu (1):
>>>>    mm: add selftests to split_huge_page() to verify unmap/zap of zero
>>>>      pages
>>>>
>>>> Usama Arif (3):
>>>>    Revert "memcg: remove mem_cgroup_uncharge_list()"
>>>>    Revert "mm: remove free_unref_page_list()"
>>>>    mm: split underutilized THPs
>>>>
>>>> Yu Zhao (2):
>>>>    mm: free zapped tail pages when splitting isolated thp
>>>>    mm: don't remap unused subpages when splitting isolated thp
>>>
>>>   I would recommend shatter [1] instead of splitting so that
>>> 1) whoever underutilized their THPs get punished for the overhead;
>>> 2) underutilized THPs are kept intact and can be reused by others.
>>>
>>> [1] https://lore.kernel.org/20240229183436.4110845-3-yuzhao@google.com/
>>
>> The objective of this series is to reduce memory usage, while trying to keep the performance benefits you get of using THP=always. Punishing any applications performance is the opposite of what I am trying to do here.
>> For e.g. if there is only one main application running in production, and its using majority of the THPs, then reducing its performance doesn't make sense.
>>
> 
> I'm not sure if there would really be a performance degradation regarding the THP, after all we zap PTEs either way.
>

By performance I meant time/CPU used up for migration.
 
> Shattering will take longer because real migration is involved IIUC.
>

Yes, so thats what I want to avoid. If the system is CPU bound like the production workload I am testing, then spending any cycles on migration is going to make time/CPU performance worse.
 
Also, shattering isn't merged upstream, and it wouldn't make sense to make this series dependent on shattering.

>> Also, just going through the commit, and found the line "The advantage of shattering is that it keeps the original THP intact" a bit confusing. I am guessing the THP is freed? i.e. if a 2M THP has 10 non-zero filled base pages and the rest are zero-filled, then after shattering we will have 10*4K memory and not 2M+10*4K? Is it the case the THP is reused at next fault?
> 
> The idea is (as I understand it) to free the full THP abck to the buddy, replacing the individual pieces that are kept to freshly allocated order-0 pages from the buddy.
>
Yu Zhao Aug. 4, 2024, 9:54 p.m. UTC | #16
On Thu, Aug 1, 2024 at 9:47 AM David Hildenbrand <david@redhat.com> wrote:
>
> On 01.08.24 08:09, Yu Zhao wrote:
> > On Tue, Jul 30, 2024 at 6:54 AM Usama Arif <usamaarif642@gmail.com> wrote:
> >>
> >> The current upstream default policy for THP is always. However, Meta
> >> uses madvise in production as the current THP=always policy vastly
> >> overprovisions THPs in sparsely accessed memory areas, resulting in
> >> excessive memory pressure and premature OOM killing.
> >> Using madvise + relying on khugepaged has certain drawbacks over
> >> THP=always. Using madvise hints mean THPs aren't "transparent" and
> >> require userspace changes. Waiting for khugepaged to scan memory and
> >> collapse pages into THP can be slow and unpredictable in terms of performance
> >> (i.e. you dont know when the collapse will happen), while production
> >> environments require predictable performance. If there is enough memory
> >> available, its better for both performance and predictability to have
> >> a THP from fault time, i.e. THP=always rather than wait for khugepaged
> >> to collapse it, and deal with sparsely populated THPs when the system is
> >> running out of memory.
> >>
> >> This patch-series is an attempt to mitigate the issue of running out of
> >> memory when THP is always enabled. During runtime whenever a THP is being
> >> faulted in or collapsed by khugepaged, the THP is added to a list.
> >> Whenever memory reclaim happens, the kernel runs the deferred_split
> >> shrinker which goes through the list and checks if the THP was underutilized,
> >> i.e. how many of the base 4K pages of the entire THP were zero-filled.
> >> If this number goes above a certain threshold, the shrinker will attempt
> >> to split that THP. Then at remap time, the pages that were zero-filled are
> >> not remapped, hence saving memory. This method avoids the downside of
> >> wasting memory in areas where THP is sparsely filled when THP is always
> >> enabled, while still providing the upside THPs like reduced TLB misses without
> >> having to use madvise.
> >>
> >> Meta production workloads that were CPU bound (>99% CPU utilzation) were
> >> tested with THP shrinker. The results after 2 hours are as follows:
> >>
> >>                              | THP=madvise |  THP=always   | THP=always
> >>                              |             |               | + shrinker series
> >>                              |             |               | + max_ptes_none=409
> >> -----------------------------------------------------------------------------
> >> Performance improvement     |      -      |    +1.8%      |     +1.7%
> >> (over THP=madvise)          |             |               |
> >> -----------------------------------------------------------------------------
> >> Memory usage                |    54.6G    | 58.8G (+7.7%) |   55.9G (+2.4%)
> >> -----------------------------------------------------------------------------
> >> max_ptes_none=409 means that any THP that has more than 409 out of 512
> >> (80%) zero filled filled pages will be split.
> >>
> >> To test out the patches, the below commands without the shrinker will
> >> invoke OOM killer immediately and kill stress, but will not fail with
> >> the shrinker:
> >>
> >> echo 450 > /sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none
> >> mkdir /sys/fs/cgroup/test
> >> echo $$ > /sys/fs/cgroup/test/cgroup.procs
> >> echo 20M > /sys/fs/cgroup/test/memory.max
> >> echo 0 > /sys/fs/cgroup/test/memory.swap.max
> >> # allocate twice memory.max for each stress worker and touch 40/512 of
> >> # each THP, i.e. vm-stride 50K.
> >> # With the shrinker, max_ptes_none of 470 and below won't invoke OOM
> >> # killer.
> >> # Without the shrinker, OOM killer is invoked immediately irrespective
> >> # of max_ptes_none value and kill stress.
> >> stress --vm 1 --vm-bytes 40M --vm-stride 50K
> >>
> >> Patches 1-2 add back helper functions that were previously removed
> >> to operate on page lists (needed by patch 3).
> >> Patch 3 is an optimization to free zapped tail pages rather than
> >> waiting for page reclaim or migration.
> >> Patch 4 is a prerequisite for THP shrinker to not remap zero-filled
> >> subpages when splitting THP.
> >> Patches 6 adds support for THP shrinker.
> >>
> >> (This patch-series restarts the work on having a THP shrinker in kernel
> >> originally done in
> >> https://lore.kernel.org/all/cover.1667454613.git.alexlzhu@fb.com/.
> >> The THP shrinker in this series is significantly different than the
> >> original one, hence its labelled v1 (although the prerequisite to not
> >> remap clean subpages is the same).)
> >>
> >> Alexander Zhu (1):
> >>    mm: add selftests to split_huge_page() to verify unmap/zap of zero
> >>      pages
> >>
> >> Usama Arif (3):
> >>    Revert "memcg: remove mem_cgroup_uncharge_list()"
> >>    Revert "mm: remove free_unref_page_list()"
> >>    mm: split underutilized THPs
> >>
> >> Yu Zhao (2):
> >>    mm: free zapped tail pages when splitting isolated thp
> >>    mm: don't remap unused subpages when splitting isolated thp
> >
> >   I would recommend shatter [1] instead of splitting so that
> > 1) whoever underutilized their THPs get punished for the overhead;
> > 2) underutilized THPs are kept intact and can be reused by others.
> >
> > [1] https://lore.kernel.org/20240229183436.4110845-3-yuzhao@google.com/
> >
>
> Do you have any plans to upstream the shattering also during "ordinary"
> deferred splitting?

Yes, once we finish verifying it in our production.
Usama Arif Aug. 4, 2024, 11:04 p.m. UTC | #17
On 01/08/2024 07:36, David Hildenbrand wrote:
>>> I just added a bunch of quick printfs to QEMU and ran a precopy+postcopy live migration. Looks like my assumption was right:
>>>
>>> On the destination:
>>>
>>> Writing received pages during precopy # ram_load_precopy()
>>> Writing received pages during precopy
>>> Writing received pages during precopy
>>> Writing received pages during precopy
>>> Writing received pages during precopy
>>> Writing received pages during precopy
>>> Writing received pages during precopy
>>> Writing received pages during precopy
>>> Writing received pages during precopy
>>> Writing received pages during precopy
>>> Writing received pages during precopy
>>> Writing received pages during precopy
>>> Writing received pages during precopy
>>> Writing received pages during precopy
>>> Writing received pages during precopy
>>> Writing received pages during precopy
>>> Writing received pages during precopy
>>> Writing received pages during precopy
>>> Disabling THP: MADV_NOHUGEPAGE # postcopy_ram_prepare_discard()
>>> Discarding pages # loadvm_postcopy_ram_handle_discard()
>>> Discarding pages
>>> Discarding pages
>>> Discarding pages
>>> Discarding pages
>>> Discarding pages
>>> Discarding pages
>>> Registering UFFD # postcopy_ram_incoming_setup()
>>>
>>
>> Thanks for this, yes it makes sense after you mentioned postcopy_ram_incoming_setup.
>> postcopy_ram_incoming_setup happens in the Listen phase, which is after the discard phase, so I was able to follow in code in qemu the same sequence of events that the above prints show.
> 
> 
> I just added another printf to postcopy_ram_supported_by_host(), where we temporarily do a UFFDIO_REGISTER on some test area.
> 
> Sensing UFFD support # postcopy_ram_supported_by_host()
> Sensing UFFD support
> Writing received pages during precopy # ram_load_precopy()
> Writing received pages during precopy
> Writing received pages during precopy
> Writing received pages during precopy
> Writing received pages during precopy
> Writing received pages during precopy
> Writing received pages during precopy
> Writing received pages during precopy
> Writing received pages during precopy
> Writing received pages during precopy
> Writing received pages during precopy
> Writing received pages during precopy
> Writing received pages during precopy
> Writing received pages during precopy
> Writing received pages during precopy
> Writing received pages during precopy
> Writing received pages during precopy
> Writing received pages during precopy
> Writing received pages during precopy
> Writing received pages during precopy
> Writing received pages during precopy
> Writing received pages during precopy
> Disabling THP: MADV_NOHUGEPAGE # postcopy_ram_prepare_discard()
> Discarding pages # loadvm_postcopy_ram_handle_discard()
> Discarding pages
> Discarding pages
> Discarding pages
> Discarding pages
> Discarding pages
> Discarding pages
> Discarding pages
> Discarding pages
> Discarding pages
> Discarding pages
> Discarding pages
> Discarding pages
> Discarding pages
> Discarding pages
> Discarding pages
> Registering UFFD # postcopy_ram_incoming_setup()
> 
> We could think about using this "ever user uffd" to avoid the shared zeropage in most processes.
> 
> Of course, there might be other applications where that wouldn't work, but I think this behavior (write to area before enabling uffd) might be fairly QEMU specific already.
> 
> Avoiding the shared zeropage has the benefit that a later write fault won't have to do a TLB flush and can simply install a fresh anon page.
> 

I checked CRIU and that does a check at the start as well before attempting to use uffd: https://github.com/checkpoint-restore/criu/blob/criu-dev/criu/kerndat.c#L1349

If writing to an area before enabling uffd is likely to be QEMU specific, then you make a good point to clear pte instead of using shared zeropage to avoid the TLB flush if uffd is ever used.

I think "ever used uffd" would need to be tracked using mm_struct. This also won't cause an issue if the check is done in a parent process and the actual use is in a forked process, as copy_mm should take care of it.
The possibilities would then be:
1) Have a new bit in mm->flags, set it in new_userfaultfd and test it in try_to_unmap_unused, but unfortunately all the bits in mm->flags are taken.
2) We could use mm->def_flags as it looks like there is an unused bit (0x800) just before VM_UFFD_WP. But that makes the code confusing as its used to initialize the default flags for VMAs and is not supposed to be used as a "mm flag".
3) Introducing mm->flags2 and set/test as 1. This would introduce a 8 byte overhead for all mm_structs.

I am not sure either 2 or 3 are acceptable upstream, unless there is a need for more flags in the near future and the 8 byte overhead starts to make sense. Maybe we go with shared zeropage?
Yu Zhao Aug. 4, 2024, 11:23 p.m. UTC | #18
On Thu, Aug 1, 2024 at 10:22 AM Usama Arif <usamaarif642@gmail.com> wrote:
>
>
>
> On 01/08/2024 07:09, Yu Zhao wrote:
> > On Tue, Jul 30, 2024 at 6:54 AM Usama Arif <usamaarif642@gmail.com> wrote:
> >>
> >> The current upstream default policy for THP is always. However, Meta
> >> uses madvise in production as the current THP=always policy vastly
> >> overprovisions THPs in sparsely accessed memory areas, resulting in
> >> excessive memory pressure and premature OOM killing.
> >> Using madvise + relying on khugepaged has certain drawbacks over
> >> THP=always. Using madvise hints mean THPs aren't "transparent" and
> >> require userspace changes. Waiting for khugepaged to scan memory and
> >> collapse pages into THP can be slow and unpredictable in terms of performance
> >> (i.e. you dont know when the collapse will happen), while production
> >> environments require predictable performance. If there is enough memory
> >> available, its better for both performance and predictability to have
> >> a THP from fault time, i.e. THP=always rather than wait for khugepaged
> >> to collapse it, and deal with sparsely populated THPs when the system is
> >> running out of memory.
> >>
> >> This patch-series is an attempt to mitigate the issue of running out of
> >> memory when THP is always enabled. During runtime whenever a THP is being
> >> faulted in or collapsed by khugepaged, the THP is added to a list.
> >> Whenever memory reclaim happens, the kernel runs the deferred_split
> >> shrinker which goes through the list and checks if the THP was underutilized,
> >> i.e. how many of the base 4K pages of the entire THP were zero-filled.
> >> If this number goes above a certain threshold, the shrinker will attempt
> >> to split that THP. Then at remap time, the pages that were zero-filled are
> >> not remapped, hence saving memory. This method avoids the downside of
> >> wasting memory in areas where THP is sparsely filled when THP is always
> >> enabled, while still providing the upside THPs like reduced TLB misses without
> >> having to use madvise.
> >>
> >> Meta production workloads that were CPU bound (>99% CPU utilzation) were
> >> tested with THP shrinker. The results after 2 hours are as follows:
> >>
> >>                             | THP=madvise |  THP=always   | THP=always
> >>                             |             |               | + shrinker series
> >>                             |             |               | + max_ptes_none=409
> >> -----------------------------------------------------------------------------
> >> Performance improvement     |      -      |    +1.8%      |     +1.7%
> >> (over THP=madvise)          |             |               |
> >> -----------------------------------------------------------------------------
> >> Memory usage                |    54.6G    | 58.8G (+7.7%) |   55.9G (+2.4%)
> >> -----------------------------------------------------------------------------
> >> max_ptes_none=409 means that any THP that has more than 409 out of 512
> >> (80%) zero filled filled pages will be split.
> >>
> >> To test out the patches, the below commands without the shrinker will
> >> invoke OOM killer immediately and kill stress, but will not fail with
> >> the shrinker:
> >>
> >> echo 450 > /sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none
> >> mkdir /sys/fs/cgroup/test
> >> echo $$ > /sys/fs/cgroup/test/cgroup.procs
> >> echo 20M > /sys/fs/cgroup/test/memory.max
> >> echo 0 > /sys/fs/cgroup/test/memory.swap.max
> >> # allocate twice memory.max for each stress worker and touch 40/512 of
> >> # each THP, i.e. vm-stride 50K.
> >> # With the shrinker, max_ptes_none of 470 and below won't invoke OOM
> >> # killer.
> >> # Without the shrinker, OOM killer is invoked immediately irrespective
> >> # of max_ptes_none value and kill stress.
> >> stress --vm 1 --vm-bytes 40M --vm-stride 50K
> >>
> >> Patches 1-2 add back helper functions that were previously removed
> >> to operate on page lists (needed by patch 3).
> >> Patch 3 is an optimization to free zapped tail pages rather than
> >> waiting for page reclaim or migration.
> >> Patch 4 is a prerequisite for THP shrinker to not remap zero-filled
> >> subpages when splitting THP.
> >> Patches 6 adds support for THP shrinker.
> >>
> >> (This patch-series restarts the work on having a THP shrinker in kernel
> >> originally done in
> >> https://lore.kernel.org/all/cover.1667454613.git.alexlzhu@fb.com/.
> >> The THP shrinker in this series is significantly different than the
> >> original one, hence its labelled v1 (although the prerequisite to not
> >> remap clean subpages is the same).)
> >>
> >> Alexander Zhu (1):
> >>   mm: add selftests to split_huge_page() to verify unmap/zap of zero
> >>     pages
> >>
> >> Usama Arif (3):
> >>   Revert "memcg: remove mem_cgroup_uncharge_list()"
> >>   Revert "mm: remove free_unref_page_list()"
> >>   mm: split underutilized THPs
> >>
> >> Yu Zhao (2):
> >>   mm: free zapped tail pages when splitting isolated thp
> >>   mm: don't remap unused subpages when splitting isolated thp
> >
> >  I would recommend shatter [1] instead of splitting so that
> > 1) whoever underutilized their THPs get punished for the overhead;
> > 2) underutilized THPs are kept intact and can be reused by others.
> >
> > [1] https://lore.kernel.org/20240229183436.4110845-3-yuzhao@google.com/
>
> The objective of this series is to reduce memory usage, while trying to keep the performance benefits you get of using THP=always.

Of course.

> Punishing any applications performance is the opposite of what I am trying to do here.

For applications that prefer THP=always, you would punish them more by
using split.

> For e.g. if there is only one main application running in production, and its using majority of the THPs, then reducing its performance doesn't make sense.

Exactly, and that's why I recommended shatter.

Let's walk through the big picture, and hopefully you'll agree.

Applications prefer THP=always because they want to allocate THPs. As
you mentioned above, the majority of their memory would be backed by
THPs, highly utilized.

You also mentioned that those applications can run into memory
pressure or even OOMs, which I agree, and this is essentially what we
are trying to solve here. Otherwise, with unlimited memory, we
wouldn't need to worry about internal fragmentation in this context.

So on one hand, we want to allocate THPs; on the other, we run into
memory pressure. It's obvious that splitting under this specific
condition can't fully solve our problem -- after splitting, we still
have to do compaction to fulfill new THP allocation requests.
Theoretically, splitting plus compaction is more expensive than
shattering itself: expressing the efficiency in
compact_success/(compact_success+fail), the latter is 100%; the former
is nowhere near it, and our experiments agree with this.

If applications opt for direct compaction, they'd pay for THP
allocation latency; if they don't want to wait, i.e., with background
compaction, but they'd pay for less THP coverage. So they are punished
either way, not in the THP shrinker path, but in their allocation
path. In comparison, shattering wins in both cases, as I explained
above.

> Also, just going through the commit, and found the line "The advantage of shattering is that it keeps the original THP intact" a bit confusing. I am guessing the THP is freed?

Yes, so that we don't have to do compaction.

> i.e. if a 2M THP has 10 non-zero filled base pages and the rest are zero-filled, then after shattering we will have 10*4K memory and not 2M+10*4K?

Correct.

> Is it the case the THP is reused at next fault?

Yes, and this is central to our condition: we are under memory
pressure with THP=always.
Yu Zhao Aug. 4, 2024, 11:32 p.m. UTC | #19
On Thu, Aug 1, 2024 at 10:27 AM David Hildenbrand <david@redhat.com> wrote:
>
> On 01.08.24 18:22, Usama Arif wrote:
> >
> >
> > On 01/08/2024 07:09, Yu Zhao wrote:
> >> On Tue, Jul 30, 2024 at 6:54 AM Usama Arif <usamaarif642@gmail.com> wrote:
> >>>
> >>> The current upstream default policy for THP is always. However, Meta
> >>> uses madvise in production as the current THP=always policy vastly
> >>> overprovisions THPs in sparsely accessed memory areas, resulting in
> >>> excessive memory pressure and premature OOM killing.
> >>> Using madvise + relying on khugepaged has certain drawbacks over
> >>> THP=always. Using madvise hints mean THPs aren't "transparent" and
> >>> require userspace changes. Waiting for khugepaged to scan memory and
> >>> collapse pages into THP can be slow and unpredictable in terms of performance
> >>> (i.e. you dont know when the collapse will happen), while production
> >>> environments require predictable performance. If there is enough memory
> >>> available, its better for both performance and predictability to have
> >>> a THP from fault time, i.e. THP=always rather than wait for khugepaged
> >>> to collapse it, and deal with sparsely populated THPs when the system is
> >>> running out of memory.
> >>>
> >>> This patch-series is an attempt to mitigate the issue of running out of
> >>> memory when THP is always enabled. During runtime whenever a THP is being
> >>> faulted in or collapsed by khugepaged, the THP is added to a list.
> >>> Whenever memory reclaim happens, the kernel runs the deferred_split
> >>> shrinker which goes through the list and checks if the THP was underutilized,
> >>> i.e. how many of the base 4K pages of the entire THP were zero-filled.
> >>> If this number goes above a certain threshold, the shrinker will attempt
> >>> to split that THP. Then at remap time, the pages that were zero-filled are
> >>> not remapped, hence saving memory. This method avoids the downside of
> >>> wasting memory in areas where THP is sparsely filled when THP is always
> >>> enabled, while still providing the upside THPs like reduced TLB misses without
> >>> having to use madvise.
> >>>
> >>> Meta production workloads that were CPU bound (>99% CPU utilzation) were
> >>> tested with THP shrinker. The results after 2 hours are as follows:
> >>>
> >>>                              | THP=madvise |  THP=always   | THP=always
> >>>                              |             |               | + shrinker series
> >>>                              |             |               | + max_ptes_none=409
> >>> -----------------------------------------------------------------------------
> >>> Performance improvement     |      -      |    +1.8%      |     +1.7%
> >>> (over THP=madvise)          |             |               |
> >>> -----------------------------------------------------------------------------
> >>> Memory usage                |    54.6G    | 58.8G (+7.7%) |   55.9G (+2.4%)
> >>> -----------------------------------------------------------------------------
> >>> max_ptes_none=409 means that any THP that has more than 409 out of 512
> >>> (80%) zero filled filled pages will be split.
> >>>
> >>> To test out the patches, the below commands without the shrinker will
> >>> invoke OOM killer immediately and kill stress, but will not fail with
> >>> the shrinker:
> >>>
> >>> echo 450 > /sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none
> >>> mkdir /sys/fs/cgroup/test
> >>> echo $$ > /sys/fs/cgroup/test/cgroup.procs
> >>> echo 20M > /sys/fs/cgroup/test/memory.max
> >>> echo 0 > /sys/fs/cgroup/test/memory.swap.max
> >>> # allocate twice memory.max for each stress worker and touch 40/512 of
> >>> # each THP, i.e. vm-stride 50K.
> >>> # With the shrinker, max_ptes_none of 470 and below won't invoke OOM
> >>> # killer.
> >>> # Without the shrinker, OOM killer is invoked immediately irrespective
> >>> # of max_ptes_none value and kill stress.
> >>> stress --vm 1 --vm-bytes 40M --vm-stride 50K
> >>>
> >>> Patches 1-2 add back helper functions that were previously removed
> >>> to operate on page lists (needed by patch 3).
> >>> Patch 3 is an optimization to free zapped tail pages rather than
> >>> waiting for page reclaim or migration.
> >>> Patch 4 is a prerequisite for THP shrinker to not remap zero-filled
> >>> subpages when splitting THP.
> >>> Patches 6 adds support for THP shrinker.
> >>>
> >>> (This patch-series restarts the work on having a THP shrinker in kernel
> >>> originally done in
> >>> https://lore.kernel.org/all/cover.1667454613.git.alexlzhu@fb.com/.
> >>> The THP shrinker in this series is significantly different than the
> >>> original one, hence its labelled v1 (although the prerequisite to not
> >>> remap clean subpages is the same).)
> >>>
> >>> Alexander Zhu (1):
> >>>    mm: add selftests to split_huge_page() to verify unmap/zap of zero
> >>>      pages
> >>>
> >>> Usama Arif (3):
> >>>    Revert "memcg: remove mem_cgroup_uncharge_list()"
> >>>    Revert "mm: remove free_unref_page_list()"
> >>>    mm: split underutilized THPs
> >>>
> >>> Yu Zhao (2):
> >>>    mm: free zapped tail pages when splitting isolated thp
> >>>    mm: don't remap unused subpages when splitting isolated thp
> >>
> >>   I would recommend shatter [1] instead of splitting so that
> >> 1) whoever underutilized their THPs get punished for the overhead;
> >> 2) underutilized THPs are kept intact and can be reused by others.
> >>
> >> [1] https://lore.kernel.org/20240229183436.4110845-3-yuzhao@google.com/
> >
> > The objective of this series is to reduce memory usage, while trying to keep the performance benefits you get of using THP=always. Punishing any applications performance is the opposite of what I am trying to do here.
> > For e.g. if there is only one main application running in production, and its using majority of the THPs, then reducing its performance doesn't make sense.
> >
>
> I'm not sure if there would really be a performance degradation
> regarding the THP, after all we zap PTEs either way.
>
> Shattering will take longer because real migration is involved IIUC.

Correct, and that's by design. Also using it in the THP shrinker path
isn't a problem.

> > Also, just going through the commit, and found the line "The advantage of shattering is that it keeps the original THP intact" a bit confusing. I am guessing the THP is freed? i.e. if a 2M THP has 10 non-zero filled base pages and the rest are zero-filled, then after shattering we will have 10*4K memory and not 2M+10*4K? Is it the case the THP is reused at next fault?
>
> The idea is (as I understand it) to free the full THP abck to the buddy,
> replacing the individual pieces that are kept to freshly allocated
> order-0 pages from the buddy.

Correct, and this is essential to our problem: we are under memory
pressure with THP=always. Under this condition, we need to compare
shatter with split + compaction, not with split alone.

To summarize, the ideal use cases are:
1. split for THP=always with unlimited memory.
2. shatter for THP=always under memory pressure.
Rik van Riel Aug. 5, 2024, 1:32 a.m. UTC | #20
On Sun, 2024-08-04 at 15:54 -0600, Yu Zhao wrote:
> On Thu, Aug 1, 2024 at 9:47 AM David Hildenbrand <david@redhat.com>
> wrote:
> > 
> > On 01.08.24 08:09, Yu Zhao wrote:
> > > 
> > >   I would recommend shatter [1] instead of splitting so that
> > > 1) whoever underutilized their THPs get punished for the
> > > overhead;
> > > 2) underutilized THPs are kept intact and can be reused by
> > > others.
> > > 
> > > [1]
> > > https://lore.kernel.org/20240229183436.4110845-3-yuzhao@google.com/
> > > 
> > 
> > Do you have any plans to upstream the shattering also during
> > "ordinary"
> > deferred splitting?
> 
> Yes, once we finish verifying it in our production.
> 

Shattering does seem like a nice improvement to the THP shrinker!

However, given that the shattering code is still being verified,
and the THP shrinker policy will no doubt need some tuning once
more real world workloads get thrown at it, would it make sense
to do those two things in parallel?

We could move forward with the THP shrinker as-is today, and use
the increased exposure it gets to fine tune the shrinking policy,
and then move it over to using the shattering code once that is
ready.

Is there any good reason to serialize these two things?
Yu Zhao Aug. 5, 2024, 7:51 p.m. UTC | #21
On Sun, Aug 4, 2024 at 7:33 PM Rik van Riel <riel@surriel.com> wrote:
>
> On Sun, 2024-08-04 at 15:54 -0600, Yu Zhao wrote:
> > On Thu, Aug 1, 2024 at 9:47 AM David Hildenbrand <david@redhat.com>
> > wrote:
> > >
> > > On 01.08.24 08:09, Yu Zhao wrote:
> > > >
> > > >   I would recommend shatter [1] instead of splitting so that
> > > > 1) whoever underutilized their THPs get punished for the
> > > > overhead;
> > > > 2) underutilized THPs are kept intact and can be reused by
> > > > others.
> > > >
> > > > [1]
> > > > https://lore.kernel.org/20240229183436.4110845-3-yuzhao@google.com/
> > > >
> > >
> > > Do you have any plans to upstream the shattering also during
> > > "ordinary"
> > > deferred splitting?
> >
> > Yes, once we finish verifying it in our production.
> >
>
> Shattering does seem like a nice improvement to the THP shrinker!
>
> However, given that the shattering code is still being verified,
> and the THP shrinker policy will no doubt need some tuning once
> more real world workloads get thrown at it, would it make sense
> to do those two things in parallel?
>
> We could move forward with the THP shrinker as-is today, and use
> the increased exposure it gets to fine tune the shrinking policy,
> and then move it over to using the shattering code once that is
> ready.
>
> Is there any good reason to serialize these two things?

I'm fine with whichever way you prefer: if you are eager to try
shattering in your production environment, I'd be incentivized to
throw in extra engineers and get it ready for you asap.
Usama Arif Aug. 6, 2024, 11:18 a.m. UTC | #22
On 05/08/2024 00:23, Yu Zhao wrote:
> On Thu, Aug 1, 2024 at 10:22 AM Usama Arif <usamaarif642@gmail.com> wrote:
>>
>>
>>
>> On 01/08/2024 07:09, Yu Zhao wrote:
>>> On Tue, Jul 30, 2024 at 6:54 AM Usama Arif <usamaarif642@gmail.com> wrote:
>>>>
>>>> The current upstream default policy for THP is always. However, Meta
>>>> uses madvise in production as the current THP=always policy vastly
>>>> overprovisions THPs in sparsely accessed memory areas, resulting in
>>>> excessive memory pressure and premature OOM killing.
>>>> Using madvise + relying on khugepaged has certain drawbacks over
>>>> THP=always. Using madvise hints mean THPs aren't "transparent" and
>>>> require userspace changes. Waiting for khugepaged to scan memory and
>>>> collapse pages into THP can be slow and unpredictable in terms of performance
>>>> (i.e. you dont know when the collapse will happen), while production
>>>> environments require predictable performance. If there is enough memory
>>>> available, its better for both performance and predictability to have
>>>> a THP from fault time, i.e. THP=always rather than wait for khugepaged
>>>> to collapse it, and deal with sparsely populated THPs when the system is
>>>> running out of memory.
>>>>
>>>> This patch-series is an attempt to mitigate the issue of running out of
>>>> memory when THP is always enabled. During runtime whenever a THP is being
>>>> faulted in or collapsed by khugepaged, the THP is added to a list.
>>>> Whenever memory reclaim happens, the kernel runs the deferred_split
>>>> shrinker which goes through the list and checks if the THP was underutilized,
>>>> i.e. how many of the base 4K pages of the entire THP were zero-filled.
>>>> If this number goes above a certain threshold, the shrinker will attempt
>>>> to split that THP. Then at remap time, the pages that were zero-filled are
>>>> not remapped, hence saving memory. This method avoids the downside of
>>>> wasting memory in areas where THP is sparsely filled when THP is always
>>>> enabled, while still providing the upside THPs like reduced TLB misses without
>>>> having to use madvise.
>>>>
>>>> Meta production workloads that were CPU bound (>99% CPU utilzation) were
>>>> tested with THP shrinker. The results after 2 hours are as follows:
>>>>
>>>>                             | THP=madvise |  THP=always   | THP=always
>>>>                             |             |               | + shrinker series
>>>>                             |             |               | + max_ptes_none=409
>>>> -----------------------------------------------------------------------------
>>>> Performance improvement     |      -      |    +1.8%      |     +1.7%
>>>> (over THP=madvise)          |             |               |
>>>> -----------------------------------------------------------------------------
>>>> Memory usage                |    54.6G    | 58.8G (+7.7%) |   55.9G (+2.4%)
>>>> -----------------------------------------------------------------------------
>>>> max_ptes_none=409 means that any THP that has more than 409 out of 512
>>>> (80%) zero filled filled pages will be split.
>>>>
>>>> To test out the patches, the below commands without the shrinker will
>>>> invoke OOM killer immediately and kill stress, but will not fail with
>>>> the shrinker:
>>>>
>>>> echo 450 > /sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none
>>>> mkdir /sys/fs/cgroup/test
>>>> echo $$ > /sys/fs/cgroup/test/cgroup.procs
>>>> echo 20M > /sys/fs/cgroup/test/memory.max
>>>> echo 0 > /sys/fs/cgroup/test/memory.swap.max
>>>> # allocate twice memory.max for each stress worker and touch 40/512 of
>>>> # each THP, i.e. vm-stride 50K.
>>>> # With the shrinker, max_ptes_none of 470 and below won't invoke OOM
>>>> # killer.
>>>> # Without the shrinker, OOM killer is invoked immediately irrespective
>>>> # of max_ptes_none value and kill stress.
>>>> stress --vm 1 --vm-bytes 40M --vm-stride 50K
>>>>
>>>> Patches 1-2 add back helper functions that were previously removed
>>>> to operate on page lists (needed by patch 3).
>>>> Patch 3 is an optimization to free zapped tail pages rather than
>>>> waiting for page reclaim or migration.
>>>> Patch 4 is a prerequisite for THP shrinker to not remap zero-filled
>>>> subpages when splitting THP.
>>>> Patches 6 adds support for THP shrinker.
>>>>
>>>> (This patch-series restarts the work on having a THP shrinker in kernel
>>>> originally done in
>>>> https://lore.kernel.org/all/cover.1667454613.git.alexlzhu@fb.com/.
>>>> The THP shrinker in this series is significantly different than the
>>>> original one, hence its labelled v1 (although the prerequisite to not
>>>> remap clean subpages is the same).)
>>>>
>>>> Alexander Zhu (1):
>>>>   mm: add selftests to split_huge_page() to verify unmap/zap of zero
>>>>     pages
>>>>
>>>> Usama Arif (3):
>>>>   Revert "memcg: remove mem_cgroup_uncharge_list()"
>>>>   Revert "mm: remove free_unref_page_list()"
>>>>   mm: split underutilized THPs
>>>>
>>>> Yu Zhao (2):
>>>>   mm: free zapped tail pages when splitting isolated thp
>>>>   mm: don't remap unused subpages when splitting isolated thp
>>>
>>>  I would recommend shatter [1] instead of splitting so that
>>> 1) whoever underutilized their THPs get punished for the overhead;
>>> 2) underutilized THPs are kept intact and can be reused by others.
>>>
>>> [1] https://lore.kernel.org/20240229183436.4110845-3-yuzhao@google.com/
>>
>> The objective of this series is to reduce memory usage, while trying to keep the performance benefits you get of using THP=always.
> 
> Of course.
> 
>> Punishing any applications performance is the opposite of what I am trying to do here.
> 
> For applications that prefer THP=always, you would punish them more by
> using split.
> 
>> For e.g. if there is only one main application running in production, and its using majority of the THPs, then reducing its performance doesn't make sense.
> 
> Exactly, and that's why I recommended shatter.
> 
> Let's walk through the big picture, and hopefully you'll agree.
> 
> Applications prefer THP=always because they want to allocate THPs. As
> you mentioned above, the majority of their memory would be backed by
> THPs, highly utilized.
> 
> You also mentioned that those applications can run into memory
> pressure or even OOMs, which I agree, and this is essentially what we
> are trying to solve here. Otherwise, with unlimited memory, we
> wouldn't need to worry about internal fragmentation in this context.
> 
> So on one hand, we want to allocate THPs; on the other, we run into
> memory pressure. It's obvious that splitting under this specific
> condition can't fully solve our problem -- after splitting, we still
> have to do compaction to fulfill new THP allocation requests.
> Theoretically, splitting plus compaction is more expensive than
> shattering itself: expressing the efficiency in
> compact_success/(compact_success+fail), the latter is 100%; the former
> is nowhere near it, and our experiments agree with this.
> 
> If applications opt for direct compaction, they'd pay for THP
> allocation latency; if they don't want to wait, i.e., with background
> compaction, but they'd pay for less THP coverage. So they are punished
> either way, not in the THP shrinker path, but in their allocation
> path. In comparison, shattering wins in both cases, as I explained
> above.

Thanks for the explanation. It makes the reason behind shattering much more clearer, and it explains why it could be an improvement over splitting.

As Rik mentioned, I think its best to parallelize the efforts of THP low utilization shrinker and shattering, as we already have the low utilization shrinker tested and ready with splitting.

I did have a question about shattering, I will mention it here but it probably is best to move the discussion over to the shattering patches you sent.

If the system is close to running out of memory and the shrinker has a very large number of folios to shatter, then the initial THPs that are shattered by shrinker will take the order-0 pages for migration of non-zero filled pages. The system could run out of order-0 pages for the latter THPs, so the THPs that were preserved earlier in the shrinker process would need to be split to provide the order-0 pages for migration needed for shattering the latter THPs?
The cost then becomes the cost of migration + the cost of splitting the THPs preserved earlier by the shrinker to provide 4K pages for migration (which means the advantage of preserving these THPs is lost?) + the cost of compaction when we later want THPs.

Hopefully I understood what would happen in the above case correctly with shattering.


> 
>> Also, just going through the commit, and found the line "The advantage of shattering is that it keeps the original THP intact" a bit confusing. I am guessing the THP is freed?
> 
> Yes, so that we don't have to do compaction.
> 
>> i.e. if a 2M THP has 10 non-zero filled base pages and the rest are zero-filled, then after shattering we will have 10*4K memory and not 2M+10*4K?
> 
> Correct.
> 
>> Is it the case the THP is reused at next fault?
> 
> Yes, and this is central to our condition: we are under memory
> pressure with THP=always.
David Hildenbrand Aug. 6, 2024, 5:33 p.m. UTC | #23
On 06.08.24 19:28, Johannes Weiner wrote:
> On Thu, Aug 01, 2024 at 08:36:32AM +0200, David Hildenbrand wrote:
>> I just added another printf to postcopy_ram_supported_by_host(), where
>> we temporarily do a UFFDIO_REGISTER on some test area.
>>
>> Sensing UFFD support # postcopy_ram_supported_by_host()
>> Sensing UFFD support
>> Writing received pages during precopy # ram_load_precopy()
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Writing received pages during precopy
>> Disabling THP: MADV_NOHUGEPAGE # postcopy_ram_prepare_discard()
>> Discarding pages # loadvm_postcopy_ram_handle_discard()
>> Discarding pages
>> Discarding pages
>> Discarding pages
>> Discarding pages
>> Discarding pages
>> Discarding pages
>> Discarding pages
>> Discarding pages
>> Discarding pages
>> Discarding pages
>> Discarding pages
>> Discarding pages
>> Discarding pages
>> Discarding pages
>> Discarding pages
>> Registering UFFD # postcopy_ram_incoming_setup()
>>
>> We could think about using this "ever user uffd" to avoid the shared
>> zeropage in most processes.
>>
>> Of course, there might be other applications where that wouldn't work,
>> but I think this behavior (write to area before enabling uffd) might be
>> fairly QEMU specific already.
> 
> It makes me a bit uneasy to hardcode this into the kernel. It's fairly
> specific to qemu/criu, and won't protect usecases that behave slightly
> differently.
> 
> It would also give userfaultfd users that aren't susceptible to this
> particular scenario a different code path.

True, but let's be honest, it's not like there are a million userfaultfd 
users out there :)

Anyhow ...

> 
>> Avoiding the shared zeropage has the benefit that a later write fault
>> won't have to do a TLB flush and can simply install a fresh anon page.
> 
> That's true - although if that happens frequently, it's something we
> might want to tune the shrinker for anyway. If subpages do get used
> later, we probably shouldn't have split the THP to begin with.
> 
> IMO the safest bet would be to use the zero page unconditionally.
> 

... I don't disagree. It also smells more like an optimization on top, 
if ever.

>>> 		return false;
>>>
>>> 	newpte = pte_mkspecial(pfn_pte(page_to_pfn(ZERO_PAGE(pvmw->address)),
>>> 					pvmw->vma->vm_page_prot));
>>>
>>> 	set_pte_at(pvmw->vma->vm_mm, pvmw->address, pvmw->pte, newpte);
>>
>> We're replacing a present page by another present page without doing a
>> TLB flush in between. I *think* this should be fine because the new
>> present page is R/O and cannot possibly be written to.
> 
> It's safe because it's replacing a migration entry. The TLB was
> flushed when that was installed, and since the migration pte is not
> marked present it couldn't have re-established a TLB entry.

Oh, right, we're dealing with a migration entry.
Johannes Weiner Aug. 6, 2024, 5:38 p.m. UTC | #24
On Thu, Aug 01, 2024 at 12:09:16AM -0600, Yu Zhao wrote:
> On Tue, Jul 30, 2024 at 6:54 AM Usama Arif <usamaarif642@gmail.com> wrote:
> >
> > The current upstream default policy for THP is always. However, Meta
> > uses madvise in production as the current THP=always policy vastly
> > overprovisions THPs in sparsely accessed memory areas, resulting in
> > excessive memory pressure and premature OOM killing.
> > Using madvise + relying on khugepaged has certain drawbacks over
> > THP=always. Using madvise hints mean THPs aren't "transparent" and
> > require userspace changes. Waiting for khugepaged to scan memory and
> > collapse pages into THP can be slow and unpredictable in terms of performance
> > (i.e. you dont know when the collapse will happen), while production
> > environments require predictable performance. If there is enough memory
> > available, its better for both performance and predictability to have
> > a THP from fault time, i.e. THP=always rather than wait for khugepaged
> > to collapse it, and deal with sparsely populated THPs when the system is
> > running out of memory.
> >
> > This patch-series is an attempt to mitigate the issue of running out of
> > memory when THP is always enabled. During runtime whenever a THP is being
> > faulted in or collapsed by khugepaged, the THP is added to a list.
> > Whenever memory reclaim happens, the kernel runs the deferred_split
> > shrinker which goes through the list and checks if the THP was underutilized,
> > i.e. how many of the base 4K pages of the entire THP were zero-filled.
> > If this number goes above a certain threshold, the shrinker will attempt
> > to split that THP. Then at remap time, the pages that were zero-filled are
> > not remapped, hence saving memory. This method avoids the downside of
> > wasting memory in areas where THP is sparsely filled when THP is always
> > enabled, while still providing the upside THPs like reduced TLB misses without
> > having to use madvise.
> >
> > Meta production workloads that were CPU bound (>99% CPU utilzation) were
> > tested with THP shrinker. The results after 2 hours are as follows:
> >
> >                             | THP=madvise |  THP=always   | THP=always
> >                             |             |               | + shrinker series
> >                             |             |               | + max_ptes_none=409
> > -----------------------------------------------------------------------------
> > Performance improvement     |      -      |    +1.8%      |     +1.7%
> > (over THP=madvise)          |             |               |
> > -----------------------------------------------------------------------------
> > Memory usage                |    54.6G    | 58.8G (+7.7%) |   55.9G (+2.4%)
> > -----------------------------------------------------------------------------
> > max_ptes_none=409 means that any THP that has more than 409 out of 512
> > (80%) zero filled filled pages will be split.
> >
> > To test out the patches, the below commands without the shrinker will
> > invoke OOM killer immediately and kill stress, but will not fail with
> > the shrinker:
> >
> > echo 450 > /sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none
> > mkdir /sys/fs/cgroup/test
> > echo $$ > /sys/fs/cgroup/test/cgroup.procs
> > echo 20M > /sys/fs/cgroup/test/memory.max
> > echo 0 > /sys/fs/cgroup/test/memory.swap.max
> > # allocate twice memory.max for each stress worker and touch 40/512 of
> > # each THP, i.e. vm-stride 50K.
> > # With the shrinker, max_ptes_none of 470 and below won't invoke OOM
> > # killer.
> > # Without the shrinker, OOM killer is invoked immediately irrespective
> > # of max_ptes_none value and kill stress.
> > stress --vm 1 --vm-bytes 40M --vm-stride 50K
> >
> > Patches 1-2 add back helper functions that were previously removed
> > to operate on page lists (needed by patch 3).
> > Patch 3 is an optimization to free zapped tail pages rather than
> > waiting for page reclaim or migration.
> > Patch 4 is a prerequisite for THP shrinker to not remap zero-filled
> > subpages when splitting THP.
> > Patches 6 adds support for THP shrinker.
> >
> > (This patch-series restarts the work on having a THP shrinker in kernel
> > originally done in
> > https://lore.kernel.org/all/cover.1667454613.git.alexlzhu@fb.com/.
> > The THP shrinker in this series is significantly different than the
> > original one, hence its labelled v1 (although the prerequisite to not
> > remap clean subpages is the same).)
> >
> > Alexander Zhu (1):
> >   mm: add selftests to split_huge_page() to verify unmap/zap of zero
> >     pages
> >
> > Usama Arif (3):
> >   Revert "memcg: remove mem_cgroup_uncharge_list()"
> >   Revert "mm: remove free_unref_page_list()"
> >   mm: split underutilized THPs
> >
> > Yu Zhao (2):
> >   mm: free zapped tail pages when splitting isolated thp
> >   mm: don't remap unused subpages when splitting isolated thp
> 
>  I would recommend shatter [1] instead of splitting so that

I agree with Rik, this seems like a possible optimization, not a
pre-requisite.

> 1) whoever underutilized their THPs get punished for the overhead;

Is that true? The downgrade is done in a shrinker. With or without
shattering, the compaction effort will be on the allocation side.

> 2) underutilized THPs are kept intact and can be reused by others.

If migration of the subpages is possible, then compaction can clear
the block as quickly as shattering can. The only difference is that
compaction would do the work on-demand, whereas shattering would do it
unconditionally, whether a THP has been requested or not...

Anyway, I think it'd be better to keep those discussions separate.