* [PATCH v10 0/6] mm/swap, memcg: Introduce swap tiers for cgroup based swap control
@ 2026-07-13 2:56 Youngjun Park
2026-07-13 2:56 ` [PATCH v10 1/6] mm: swap: introduce swap tier infrastructure Youngjun Park
` (6 more replies)
0 siblings, 7 replies; 20+ messages in thread
From: Youngjun Park @ 2026-07-13 2:56 UTC (permalink / raw)
To: akpm
Cc: chrisl, youngjun.park, linux-mm, cgroups, linux-kernel, kasong,
hannes, mhocko, roman.gushchin, shakeel.butt, muchun.song,
shikemeng, baoquan.he, baohua, yosry, joshua.hahnjy, gunho.lee,
taejoon.song, hyungjun.cho, baver.bae, her0gyugyu
This is the v10 series of the swap tier patchset.
v10 folds in the Sashiko review fixes for the selftests added in v9 and
rebases onto the current mm-new. There are no functional changes to the
core swap or memcg code since v9; see the changelog for details.
For context, the bulk of the series is unchanged since v8, with great thanks
to Shakeel Butt and Yosry for the reviews and discussions [1] that shaped it.
The main change in v8 was the interface change to use memory.swap.tiers.max
with '0' (disable) and 'max' (enable) values. This mechanism was suggested
by Shakeel and Yosry.
This change allows for future extensions to control swap between tiers and
aligns better with existing memcg interfaces. It is confined to patch #3's
user-facing interface; internally, patch #3 still uses the existing mask
processing method, which is implementation-efficient.
We also discussed tier extensions. Thanks to Yosry, Nhat and Shakeel for their
valuable feedback.
Here is a brief summary of our tentative conclusions. Please correct me
if anything is misrepresented (details in references):
* Zswap tiering [2]:
Zswap can itself be a tier (typically the fastest one). But, until vswap lands,
zswap cannot be the only allowed tier,
since it still needs a physical device for allocation;
that restriction can be lifted once vswap is supported.
* Vswap tiering [3]:
Vswap should be handled transparently to the user. Vswap itself will
not be tiered. But, someday supported if there is strong and real usecase.
* Relationship with zswap.writeback [4]:
If zswap tiering is introduced, it could replace the zswap-only tier.
However, since zswap cannot be tiered independently without vswap, it is still
needed for non-vswap cases.
* Tier demotion [5]:
A separate interface like memory.swap.tiers.demotion might be needed.
For now, we only support 0/max to enable/disable tiers. A future "auto"
mode could scale a per-tier limit from swapfile size and memory.swap.max,
similar to the direction memory tiering is heading in; the exact
default-scaling behaviour is still under discussion.
I plan to apply the swap tier infrastructure and the first use case
(cgroup-based swap control) first, and continue following up on the
discussions above.
Overview
========
Swap Tiers group swap devices into performance classes (e.g. NVMe,
HDD, Network) and allow per-memcg selection of which tiers to use.
This mechanism was suggested by Chris Li.
Design Rationale
================
Swap tier selection is attached to memcg. A child cgroup may select a
subset of the parent's allowed tiers.
This
- Preserves cgroup inheritance semantics (boundary at parent,
refinement at child).
- Reuses memcg, which already groups processes and enforces
hierarchical memory limits.
- Aligns with existing memcg swap controls (e.g. swap.max, zswap.writeback)
- Avoids introducing a parallel swap control hierarchy.
Placing tier control outside memcg (e.g., via BPF, syscalls, or
madvise) would allow swap preference to diverge from the memcg
hierarchy. Integrating it into memcg keeps the swap policy
consistent with existing memory ownership semantics. There are
also real use cases built around memcg.
In the future, this can be extended to other interfaces to cover
additional use cases.
I believe a memcg-based swap control is a good starting point
before such extensions.
Use Cases
=========
#1: Latency separation (our primary deployment scenario)
[ / ]
|
+-- latency-sensitive workload (fast tier)
+-- background workload (slow tier)
The parent defines the memory boundary.
Each workload selects a swap tier via memory.swap.tiers.max according to
latency requirements.
This prevents latency-sensitive workloads from being swapped to
slow devices used by background workloads.
#2: Per-VM swap selection (Chris Li's deployment scenario)
[ / ]
|
+-- [ Job on VM ] (tiers: zswap, SSD)
|
+-- [ VMM guest memory ] (tiers: SSD)
The parent (job) has access to both zswap and SSD tiers.
The child (VMM guest memory) selects SSD as its swap tier via
memory.swap.tiers.max. In this deployment, swap device selection
happens at the child level from the parent's available set.
#3: Tier isolation for reduced contention (hypothetical)
[ / ] (tiers: A, B)
|
+-- workload X (tiers: A)
+-- workload Y (tiers: B)
Each child uses a different tier. Since swap paths are separated
per tier, synchronization overhead between the two workloads is
reduced.
Future extension (Follow up)
============================
#1: Intra-tier distribution policy:
Currently, swap devices with the same priority are allocated in a
round-robin fashion. Per-tier policy files under
/sys/kernel/mm/swap/tiers/ can control how devices within a tier
are selected (e.g. round-robin, weighted).
#2: Inter-tier promotion and demotion:
Promotion and demotion apply between tiers, not within a single
tier. The current interface defines only tier assignment; it does
not yet define when or how pages move between tiers. Two triggering
models are possible:
(a) User-triggered: userspace explicitly initiates migration between
tiers (e.g. via a new interface or existing move_pages semantics).
(b) Kernel-triggered: the kernel moves pages between tiers at
appropriate points such as reclaim or refault.
#3: Per-VMA, per-process swap and BPF:
Not just for memcg based swap, possible to extend Per-VMA or per-process
swap. Or we can use it as BPF program.
#4: Zswap and vswap tiering:
Tiering applies to the vswap + zswap combination.
#5: Vswap on/off control:
Currently not supported. If a strong use case arises where vswap needs
to be controlled by memcg, the tier interface could be used for it.
#6: Per-CPU swap allocation caching:
Per-si/per-tier per-CPU caching of allocations to reduce contention in
the tier-filtered allocation path.
Experimentation
===============
Tested on our internal platform using NBD as a separate swap tier.
Our first production's simple usecase.
Without tiers:
- No selective control over flash wear
- Cannot selectively assign NBD to specific applications
Cold launch improvement (preloaded vs. baseline):
- App A: 13.17s -> 4.18s (68%)
- App B: 5.60s -> 1.12s (80%)
- App C: 10.25s -> 2.00s (80%)
Performance impact with no tiers configured:
<1% regression in kernel build and vm-scalability benchmarks
Change log
===========
v10
- selftests: applied the Sashiko review fixes, and dropped redundant comments and dead code.
(#5, #6 patches)
- Rebased on recent mm-new.
- v9 link: https://lore.kernel.org/linux-mm/20260620181635.299364-1-youngjun.park@lge.com/
v9
- Added selftests (per Nhat's request):
- selftests/mm: swap tier configuration test for /sys/kernel/mm/swap/tiers.(#5 patch)
- selftests/cgroup: swap tier routing test for memory.swap.tiers.max. (#6 patch)
- Removed the redundant rcu_read_lock() around the memcg tier-mask tree walk;
for_each_mem_cgroup_tree() already takes RCU internally and returns each
memcg with a reference held. (#3 patch)
- Sashiko review: swap_sync_discard() now honors the memcg tier mask, so the
discard fallback no longer drains clusters on disallowed tiers. Left as-is:
the cgroup tree walk under spinlock (bounded by cgroup.max.descendants, an
admin-controlled limit, and triggered only by infrequent tier writes) and
the pre-existing swap_avail_lock drop in swap_alloc_slow(). (#4 patch)
- Dropped patch #4's Reviewed-by tags (Nhat, Kairui, Baoquan): the
swap_sync_discard() change above modifies that patch (the tier mask is now
passed as a parameter into the alloc and discard paths), so the earlier tags
no longer apply. Re-review would be welcome.
- v8 link: https://lore.kernel.org/linux-mm/20260617053447.2831896-1-youngjun.park@lge.com/
v8
- Changed the memcg interface to memory.swap.tiers.max.
Values are '0' (disable) and 'max' (enable). Default is 'max'.
- Addressed Sashiko's review: Update the mask value atomically at once and
read the mask value while grabbing lock.
- Collected review tags from Kairui and Nhat.
- Rebase on recent mm-new
- v7 link: https://lore.kernel.org/linux-mm/20260527062247.3440692-1-youngjun.park@lge.com/
v7
- Collect Baoquan's review tag
- Baoquan's feedback on fixing improper comment
- Minor code adjustments per Baoquan's feedback.
- Rebase on recent mm-new
- v6 link: https://lore.kernel.org/linux-mm/20260421055323.940344-1-youngjun.park@lge.com/
v6
- Sashiko AI review fixes
- Fix batch parsing error path to restore snapshot before exit
- Reject overlong tier names to prevent truncated duplicates
- Avoid restoring raw list_head via memcpy (stale pointer risk)
- Ensure early parse errors do not skip DEF_SWAP_PRIO validation
- Use (1U << TIER_DEFAULT_IDX) to avoid signed shift UB
- Defer tier mask inheritance to css_online() to close race window
- Add READ_ONCE()/WRITE_ONCE() for tier mask accesses
- Other fixes
- Fix build error reintroduced due to missing v5 change
- Fix WARNING in folio_tier_effective_mask by adding rcu_read_lock()
- default number of swap tier max (change to 32->31, for reserving last bit)
- commit message refinement.
- rebased on recently mm-new
- v5 link: https://lore.kernel.org/linux-mm/20260325175453.2523280-1-youngjun.park@lge.com/
v5
- Fixed build errors reported in v4
- rebased on up to date mm-new
- Minor cleanups
- Design docs with validation (by Shakeel Butt discussion)
- v4 link : https://lore.kernel.org/linux-mm/20260217000950.4015880-1-youngjun.park@lge.com/
v4
- Simplified control flow and indentation
- Added CONFIG option for MAX_SWAPTIER (default: 4)
- Added memory.swap.tiers.effective interface
- Reworked save/restore logic into snapshot/rollback model
- Removed tier priority modification support (deferred)
- Improved validation and fixed edge cases
- Rebased onto latest mm-new
- RFC v3 link: https://lore.kernel.org/linux-mm/20260131125454.3187546-1-youngjun.park@lge.com/
RFC v1 ~ v3
- Change the direction after discussion with Chris-Li
- apply some LPC feedback.
- RFC v2 - https://lore.kernel.org/linux-mm/20260126065242.1221862-1-youngjun.park@lge.com/
- RFC v1 - https://lore.kernel.org/linux-mm/20251109124947.1101520-1-youngjun.park@lge.com/
Earlier Approach (per cgroup swap priority)
- v1: https://lore.kernel.org/linux-mm/20250716202006.3640584-1-youngjun.park@lge.com/
- RFC: https://lore.kernel.org/linux-mm/aEvLjEInMQC7hEyh@yjaykim-PowerEdge-T330/T/#mbbb6a5e9e30843097e1f5f65fb98f31d582b973d
Reference
=========
[1] https://lore.kernel.org/linux-doc/aiw2p5ANjsQUCIHA@linux.dev/
[2] https://lore.kernel.org/linux-mm/CAKEwX=Nz9SWcEVQGQjHN8P8OANJY4BG0w+iQOzoNOWuteoVjAg@mail.gmail.com/
[3] https://lore.kernel.org/cgroups/CAKEwX=O23a4iWBZoewKVb8QqODte6r3Xijckw3_oCJNoiO9M5A@mail.gmail.com/
[4] https://lore.kernel.org/linux-mm/CAO9r8zOg0OP1Ak1v7CRzSfQq0D8b4Dw+_T0Jui6YTM_KwQQNOA@mail.gmail.com/
[5] https://lore.kernel.org/linux-mm/CAO9r8zNi4-QC4sUi=xXWHt9WMeG39mbyoSf8kON9vLOZ=cbCmw@mail.gmail.com/
Youngjun Park (6):
mm: swap: introduce swap tier infrastructure
mm: swap: associate swap devices with tiers
mm: memcontrol: add interface for swap tier selection
mm: swap: filter swap allocation by memcg tier mask
selftests/mm: add a swap tier configuration test
selftests/cgroup: add a swap tier routing test
Documentation/admin-guide/cgroup-v2.rst | 20 +
Documentation/mm/index.rst | 1 +
Documentation/mm/swap-tier.rst | 159 ++++++
MAINTAINERS | 3 +
include/linux/memcontrol.h | 5 +
include/linux/swap.h | 1 +
mm/Kconfig | 12 +
mm/Makefile | 2 +-
mm/memcontrol.c | 67 +++
mm/swap.h | 4 +
mm/swap_state.c | 75 +++
mm/swap_tier.c | 477 ++++++++++++++++
mm/swap_tier.h | 76 +++
mm/swapfile.c | 34 +-
tools/testing/selftests/cgroup/.gitignore | 1 +
tools/testing/selftests/cgroup/Makefile | 2 +
tools/testing/selftests/cgroup/config | 2 +
.../selftests/cgroup/test_swap_tiers.c | 509 ++++++++++++++++++
tools/testing/selftests/mm/.gitignore | 1 +
tools/testing/selftests/mm/Makefile | 1 +
tools/testing/selftests/mm/config | 2 +
tools/testing/selftests/mm/run_vmtests.sh | 5 +
tools/testing/selftests/mm/swap_tier.c | 337 ++++++++++++
23 files changed, 1785 insertions(+), 11 deletions(-)
create mode 100644 Documentation/mm/swap-tier.rst
create mode 100644 mm/swap_tier.c
create mode 100644 mm/swap_tier.h
create mode 100644 tools/testing/selftests/cgroup/test_swap_tiers.c
create mode 100644 tools/testing/selftests/mm/swap_tier.c
base-commit: 61cccb8363fcc282d4ae0555b8739dd227f5ad0b
--
2.34.1
^ permalink raw reply [flat|nested] 20+ messages in thread
* [PATCH v10 1/6] mm: swap: introduce swap tier infrastructure
2026-07-13 2:56 [PATCH v10 0/6] mm/swap, memcg: Introduce swap tiers for cgroup based swap control Youngjun Park
@ 2026-07-13 2:56 ` Youngjun Park
2026-07-13 2:56 ` [PATCH v10 2/6] mm: swap: associate swap devices with tiers Youngjun Park
` (5 subsequent siblings)
6 siblings, 0 replies; 20+ messages in thread
From: Youngjun Park @ 2026-07-13 2:56 UTC (permalink / raw)
To: akpm
Cc: chrisl, youngjun.park, linux-mm, cgroups, linux-kernel, kasong,
hannes, mhocko, roman.gushchin, shakeel.butt, muchun.song,
shikemeng, baoquan.he, baohua, yosry, joshua.hahnjy, gunho.lee,
taejoon.song, hyungjun.cho, baver.bae, her0gyugyu
This patch introduces the "Swap tier" concept, which serves as an
abstraction layer for managing swap devices based on their performance
characteristics (e.g., NVMe, HDD, Network swap).
Swap tiers are user-named groups representing priority ranges.
Tier names must consist of alphanumeric characters and underscores.
These tiers collectively cover the entire priority space from -1
(`DEF_SWAP_PRIO`) to `SHRT_MAX`.
To configure tiers, a new sysfs interface is exposed at
/sys/kernel/mm/swap/tiers. The input parser evaluates commands from
left to right and supports batch input, allowing users to add or remove
multiple tiers in a single write operation.
Tier management enforces continuous priority ranges anchored by start
priorities. Operations trigger range splitting or merging, but overwriting
start priorities is forbidden. Merging expands lower tiers upwards to
preserve configured start priorities, except when removing `DEF_SWAP_PRIO`,
which merges downwards.
Suggested-by: Chris Li <chrisl@kernel.org>
Reviewed-by: Baoquan He <baoquan.he@linux.dev>
Signed-off-by: Youngjun Park <youngjun.park@lge.com>
---
MAINTAINERS | 2 +
mm/Kconfig | 12 ++
mm/Makefile | 2 +-
mm/swap.h | 4 +
mm/swap_state.c | 74 ++++++++++++
mm/swap_tier.c | 302 ++++++++++++++++++++++++++++++++++++++++++++++++
mm/swap_tier.h | 20 ++++
mm/swapfile.c | 8 +-
8 files changed, 420 insertions(+), 4 deletions(-)
create mode 100644 mm/swap_tier.c
create mode 100644 mm/swap_tier.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 8813d5d7eb0c..e94d1af17c39 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -17244,6 +17244,8 @@ F: mm/page_io.c
F: mm/swap.h
F: mm/swap_table.h
F: mm/swap_state.c
+F: mm/swap_tier.c
+F: mm/swap_tier.h
F: mm/swapfile.c
MEMORY MANAGEMENT - THP (TRANSPARENT HUGE PAGE)
diff --git a/mm/Kconfig b/mm/Kconfig
index 69c4247306cc..0b6ff5d882dc 100644
--- a/mm/Kconfig
+++ b/mm/Kconfig
@@ -19,6 +19,18 @@ menuconfig SWAP
used to provide more virtual memory than the actual RAM present
in your computer. If unsure say Y.
+config NR_SWAP_TIERS
+ int "Number of swap device tiers"
+ depends on SWAP
+ default 4
+ range 1 31
+ help
+ Sets the number of swap device tiers. Swap devices are
+ grouped into tiers based on their priority, allowing the
+ system to prefer faster devices over slower ones.
+
+ If unsure, say 4.
+
config ZSWAP
bool "Compressed cache for swap pages"
depends on SWAP
diff --git a/mm/Makefile b/mm/Makefile
index ab37ef428d98..6c7522d04485 100644
--- a/mm/Makefile
+++ b/mm/Makefile
@@ -75,7 +75,7 @@ ifdef CONFIG_MMU
obj-$(CONFIG_ADVISE_SYSCALLS) += madvise.o
endif
-obj-$(CONFIG_SWAP) += page_io.o swap_state.o swapfile.o
+obj-$(CONFIG_SWAP) += page_io.o swap_state.o swapfile.o swap_tier.o
obj-$(CONFIG_ZSWAP) += zswap.o
obj-$(CONFIG_HAS_DMA) += dmapool.o
obj-$(CONFIG_HUGETLBFS) += hugetlb.o hugetlb_sysfs.o hugetlb_sysctl.o
diff --git a/mm/swap.h b/mm/swap.h
index b51ad3071a73..7ceab672a860 100644
--- a/mm/swap.h
+++ b/mm/swap.h
@@ -32,6 +32,10 @@ struct swap_memcg_table;
#define swap_entry_order(order) 0
#endif
+#define DEF_SWAP_PRIO -1
+
+extern spinlock_t swap_lock;
+extern struct plist_head swap_active_head;
extern struct swap_info_struct *swap_info[];
/*
diff --git a/mm/swap_state.c b/mm/swap_state.c
index 1444d20a40e9..c18ff741f2e0 100644
--- a/mm/swap_state.c
+++ b/mm/swap_state.c
@@ -26,6 +26,7 @@
#include "internal.h"
#include "swap_table.h"
#include "swap.h"
+#include "swap_tier.h"
/* Swap readahead cluster size, as a power of 2 pages. */
static int page_cluster;
@@ -1039,8 +1040,81 @@ static ssize_t vma_ra_enabled_store(struct kobject *kobj,
}
static struct kobj_attribute vma_ra_enabled_attr = __ATTR_RW(vma_ra_enabled);
+static ssize_t tiers_show(struct kobject *kobj,
+ struct kobj_attribute *attr, char *buf)
+{
+ return swap_tiers_sysfs_show(buf);
+}
+
+static ssize_t tiers_store(struct kobject *kobj,
+ struct kobj_attribute *attr,
+ const char *buf, size_t count)
+{
+ char *p, *token, *name, *tmp;
+ int ret = 0;
+ short prio;
+
+ tmp = kstrdup(buf, GFP_KERNEL);
+ if (!tmp)
+ return -ENOMEM;
+
+ spin_lock(&swap_lock);
+ spin_lock(&swap_tier_lock);
+ swap_tiers_snapshot();
+
+ p = tmp;
+ while ((token = strsep(&p, ", \t\n")) != NULL) {
+ if (!*token)
+ continue;
+
+ switch (token[0]) {
+ case '+':
+ name = token + 1;
+ token = strchr(name, ':');
+ if (!token) {
+ ret = -EINVAL;
+ goto restore;
+ }
+ *token++ = '\0';
+ if (kstrtos16(token, 10, &prio)) {
+ ret = -EINVAL;
+ goto restore;
+ }
+ ret = swap_tiers_add(name, prio);
+ if (ret)
+ goto restore;
+ break;
+ case '-':
+ ret = swap_tiers_remove(token + 1);
+ if (ret)
+ goto restore;
+ break;
+ default:
+ ret = -EINVAL;
+ goto restore;
+ }
+ }
+
+ if (!swap_tiers_validate()) {
+ ret = -EINVAL;
+ goto restore;
+ }
+ goto out;
+
+restore:
+ swap_tiers_snapshot_restore();
+out:
+ spin_unlock(&swap_tier_lock);
+ spin_unlock(&swap_lock);
+ kfree(tmp);
+ return ret ? ret : count;
+}
+
+static struct kobj_attribute tier_attr = __ATTR_RW(tiers);
+
static struct attribute *swap_attrs[] = {
&vma_ra_enabled_attr.attr,
+ &tier_attr.attr,
NULL,
};
diff --git a/mm/swap_tier.c b/mm/swap_tier.c
new file mode 100644
index 000000000000..ac7a3c2a48cb
--- /dev/null
+++ b/mm/swap_tier.c
@@ -0,0 +1,302 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <linux/swap.h>
+#include <linux/memcontrol.h>
+#include "memcontrol-v1.h"
+#include <linux/sysfs.h>
+#include <linux/plist.h>
+
+#include "swap.h"
+#include "swap_tier.h"
+
+#define MAX_SWAPTIER CONFIG_NR_SWAP_TIERS
+#define MAX_TIERNAME 16
+
+/*
+ * struct swap_tier - structure representing a swap tier.
+ *
+ * @name: name of the swap_tier.
+ * @prio: starting value of priority.
+ * @list: linked list of tiers.
+ */
+static struct swap_tier {
+ char name[MAX_TIERNAME];
+ short prio;
+ struct list_head list;
+} swap_tiers[MAX_SWAPTIER];
+
+DEFINE_SPINLOCK(swap_tier_lock);
+/* active swap priority list, sorted in descending order */
+static LIST_HEAD(swap_tier_active_list);
+/* unused swap_tier object */
+static LIST_HEAD(swap_tier_inactive_list);
+
+#define TIER_IDX(tier) ((tier) - swap_tiers)
+#define TIER_MASK(tier) (1U << TIER_IDX(tier))
+#define TIER_INACTIVE_PRIO (DEF_SWAP_PRIO - 1)
+#define TIER_IS_ACTIVE(tier) ((tier->prio) != TIER_INACTIVE_PRIO)
+#define TIER_END_PRIO(tier) \
+ (!list_is_first(&(tier)->list, &swap_tier_active_list) ? \
+ list_prev_entry((tier), list)->prio - 1 : SHRT_MAX)
+
+#define for_each_tier(tier, idx) \
+ for (idx = 0, tier = &swap_tiers[0]; idx < MAX_SWAPTIER; \
+ idx++, tier = &swap_tiers[idx])
+
+#define for_each_active_tier(tier) \
+ list_for_each_entry(tier, &swap_tier_active_list, list)
+
+#define for_each_inactive_tier(tier) \
+ list_for_each_entry(tier, &swap_tier_inactive_list, list)
+
+/*
+ * Naming Convention:
+ * swap_tiers_*() - Public/exported functions
+ * swap_tier_*() - Private/internal functions
+ */
+
+static bool swap_tier_is_active(void)
+{
+ return !list_empty(&swap_tier_active_list);
+}
+
+static struct swap_tier *swap_tier_lookup(const char *name)
+{
+ struct swap_tier *tier;
+
+ for_each_active_tier(tier) {
+ if (!strcmp(tier->name, name))
+ return tier;
+ }
+
+ return NULL;
+}
+
+/* Insert new tier into the active list sorted by priority. */
+static void swap_tier_activate(struct swap_tier *new)
+{
+ struct list_head *pos = &swap_tier_active_list;
+ struct swap_tier *tier;
+
+ for_each_active_tier(tier) {
+ if (tier->prio <= new->prio) {
+ pos = &tier->list;
+ break;
+ }
+ }
+
+ list_add_tail(&new->list, pos);
+}
+
+static void swap_tier_inactivate(struct swap_tier *tier)
+{
+ list_move(&tier->list, &swap_tier_inactive_list);
+ tier->prio = TIER_INACTIVE_PRIO;
+}
+
+void swap_tiers_init(void)
+{
+ struct swap_tier *tier;
+ int idx;
+
+ BUILD_BUG_ON(BITS_PER_TYPE(int) < MAX_SWAPTIER);
+
+ for_each_tier(tier, idx) {
+ INIT_LIST_HEAD(&tier->list);
+ swap_tier_inactivate(tier);
+ }
+}
+
+ssize_t swap_tiers_sysfs_show(char *buf)
+{
+ struct swap_tier *tier;
+ ssize_t len = 0;
+
+ len += sysfs_emit_at(buf, len, "%-16s %-5s %-11s %-11s\n",
+ "Name", "Idx", "PrioStart", "PrioEnd");
+
+ spin_lock(&swap_tier_lock);
+ for_each_active_tier(tier) {
+ len += sysfs_emit_at(buf, len, "%-16s %-5td %-11d %-11d\n",
+ tier->name,
+ TIER_IDX(tier),
+ tier->prio,
+ TIER_END_PRIO(tier));
+ }
+ spin_unlock(&swap_tier_lock);
+
+ return len;
+}
+
+static struct swap_tier *swap_tier_prepare(const char *name, short prio)
+{
+ struct swap_tier *tier;
+
+ lockdep_assert_held(&swap_tier_lock);
+
+ if (prio < DEF_SWAP_PRIO)
+ return ERR_PTR(-EINVAL);
+
+ if (list_empty(&swap_tier_inactive_list))
+ return ERR_PTR(-ENOSPC);
+
+ tier = list_first_entry(&swap_tier_inactive_list,
+ struct swap_tier, list);
+
+ list_del_init(&tier->list);
+ strscpy(tier->name, name, MAX_TIERNAME);
+ tier->prio = prio;
+
+ return tier;
+}
+
+static int swap_tier_check_range(short prio)
+{
+ struct swap_tier *tier;
+
+ lockdep_assert_held(&swap_lock);
+ lockdep_assert_held(&swap_tier_lock);
+
+ for_each_active_tier(tier) {
+ /* No overwrite */
+ if (tier->prio == prio)
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static bool swap_tier_validate_name(const char *name)
+{
+ int len;
+
+ if (!name || !*name)
+ return false;
+
+ len = strlen(name);
+ if (len >= MAX_TIERNAME)
+ return false;
+
+ while (*name) {
+ if (!isalnum(*name) && *name != '_')
+ return false;
+ name++;
+ }
+ return true;
+}
+
+int swap_tiers_add(const char *name, int prio)
+{
+ int ret;
+ struct swap_tier *tier;
+
+ lockdep_assert_held(&swap_lock);
+ lockdep_assert_held(&swap_tier_lock);
+
+ /* Duplicate check */
+ if (swap_tier_lookup(name))
+ return -EEXIST;
+
+ if (!swap_tier_validate_name(name))
+ return -EINVAL;
+
+ ret = swap_tier_check_range(prio);
+ if (ret)
+ return ret;
+
+ tier = swap_tier_prepare(name, prio);
+ if (IS_ERR(tier)) {
+ ret = PTR_ERR(tier);
+ return ret;
+ }
+
+ swap_tier_activate(tier);
+
+ return ret;
+}
+
+int swap_tiers_remove(const char *name)
+{
+ int ret = 0;
+ struct swap_tier *tier;
+
+ lockdep_assert_held(&swap_lock);
+ lockdep_assert_held(&swap_tier_lock);
+
+ tier = swap_tier_lookup(name);
+ if (!tier)
+ return -EINVAL;
+
+ /* Removing DEF_SWAP_PRIO merges into the higher tier. */
+ if (!list_is_singular(&swap_tier_active_list)
+ && tier->prio == DEF_SWAP_PRIO)
+ list_prev_entry(tier, list)->prio = DEF_SWAP_PRIO;
+
+ swap_tier_inactivate(tier);
+
+ return ret;
+}
+
+static struct swap_tier swap_tiers_snap[MAX_SWAPTIER];
+/*
+ * XXX: When multiple operations (adds and removes) are submitted in a
+ * single write, reverting each individually on failure is complex and
+ * error-prone. Instead, snapshot the entire state beforehand and
+ * restore it wholesale if any operation fails.
+ */
+void swap_tiers_snapshot(void)
+{
+ BUILD_BUG_ON(sizeof(swap_tiers_snap) != sizeof(swap_tiers));
+
+ lockdep_assert_held(&swap_lock);
+ lockdep_assert_held(&swap_tier_lock);
+
+ memcpy(swap_tiers_snap, swap_tiers, sizeof(swap_tiers));
+}
+
+void swap_tiers_snapshot_restore(void)
+{
+ struct swap_tier *tier;
+ int idx;
+
+ lockdep_assert_held(&swap_lock);
+ lockdep_assert_held(&swap_tier_lock);
+
+ memcpy(swap_tiers, swap_tiers_snap, sizeof(swap_tiers));
+
+ INIT_LIST_HEAD(&swap_tier_active_list);
+ INIT_LIST_HEAD(&swap_tier_inactive_list);
+
+ /*
+ * memcpy copied snapshot-time list pointers into each tier's
+ * list_head. Those references are stale, so re-init every
+ * tier before re-linking into the freshly initialised global
+ * lists below.
+ */
+ for_each_tier(tier, idx) {
+ INIT_LIST_HEAD(&tier->list);
+
+ if (TIER_IS_ACTIVE(tier))
+ swap_tier_activate(tier);
+ else
+ swap_tier_inactivate(tier);
+ }
+}
+
+bool swap_tiers_validate(void)
+{
+ struct swap_tier *tier;
+
+ /*
+ * Initial setting might not cover DEF_SWAP_PRIO.
+ * Swap tier must cover the full range (DEF_SWAP_PRIO to SHRT_MAX).
+ */
+ if (swap_tier_is_active()) {
+ tier = list_last_entry(&swap_tier_active_list,
+ struct swap_tier, list);
+
+ if (tier->prio != DEF_SWAP_PRIO)
+ return false;
+ }
+
+ return true;
+}
diff --git a/mm/swap_tier.h b/mm/swap_tier.h
new file mode 100644
index 000000000000..a1395ec02c24
--- /dev/null
+++ b/mm/swap_tier.h
@@ -0,0 +1,20 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _SWAP_TIER_H
+#define _SWAP_TIER_H
+
+#include <linux/types.h>
+#include <linux/spinlock.h>
+
+extern spinlock_t swap_tier_lock;
+
+/* Initialization and application */
+void swap_tiers_init(void);
+ssize_t swap_tiers_sysfs_show(char *buf);
+
+int swap_tiers_add(const char *name, int prio);
+int swap_tiers_remove(const char *name);
+
+void swap_tiers_snapshot(void);
+void swap_tiers_snapshot_restore(void);
+bool swap_tiers_validate(void);
+#endif /* _SWAP_TIER_H */
diff --git a/mm/swapfile.c b/mm/swapfile.c
index 72952491e9cf..ff567ad893a4 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -48,6 +48,7 @@
#include "swap_table.h"
#include "internal.h"
#include "swap.h"
+#include "swap_tier.h"
static void swap_range_alloc(struct swap_info_struct *si,
unsigned int nr_entries);
@@ -63,7 +64,8 @@ static void move_cluster(struct swap_info_struct *si,
*
* Also protects swap_active_head total_swap_pages, and the SWP_WRITEOK flag.
*/
-static DEFINE_SPINLOCK(swap_lock);
+DEFINE_SPINLOCK(swap_lock);
+
static unsigned int nr_swapfiles;
atomic_long_t nr_swap_pages;
/*
@@ -74,7 +76,6 @@ atomic_long_t nr_swap_pages;
EXPORT_SYMBOL_GPL(nr_swap_pages);
/* protected with swap_lock. reading in vm_swap_full() doesn't need lock */
long total_swap_pages;
-#define DEF_SWAP_PRIO -1
unsigned long swapfile_maximum_size;
#ifdef CONFIG_MIGRATION
bool swap_migration_ad_supported;
@@ -87,7 +88,7 @@ static const char Bad_offset[] = "Bad swap offset entry ";
* all active swap_info_structs
* protected with swap_lock, and ordered by priority.
*/
-static PLIST_HEAD(swap_active_head);
+PLIST_HEAD(swap_active_head);
/*
* all available (active, not full) swap_info_structs
@@ -3993,6 +3994,7 @@ static int __init swapfile_init(void)
swap_migration_ad_supported = true;
#endif /* CONFIG_MIGRATION */
+ swap_tiers_init();
return 0;
}
subsys_initcall(swapfile_init);
--
2.34.1
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH v10 2/6] mm: swap: associate swap devices with tiers
2026-07-13 2:56 [PATCH v10 0/6] mm/swap, memcg: Introduce swap tiers for cgroup based swap control Youngjun Park
2026-07-13 2:56 ` [PATCH v10 1/6] mm: swap: introduce swap tier infrastructure Youngjun Park
@ 2026-07-13 2:56 ` Youngjun Park
2026-07-13 14:28 ` Usama Arif
2026-07-13 2:56 ` [PATCH v10 3/6] mm: memcontrol: add interface for swap tier selection Youngjun Park
` (4 subsequent siblings)
6 siblings, 1 reply; 20+ messages in thread
From: Youngjun Park @ 2026-07-13 2:56 UTC (permalink / raw)
To: akpm
Cc: chrisl, youngjun.park, linux-mm, cgroups, linux-kernel, kasong,
hannes, mhocko, roman.gushchin, shakeel.butt, muchun.song,
shikemeng, baoquan.he, baohua, yosry, joshua.hahnjy, gunho.lee,
taejoon.song, hyungjun.cho, baver.bae, her0gyugyu
This patch connects swap devices to the swap tier infrastructure,
ensuring that devices are correctly assigned to tiers based on their
priority.
A `tier_mask` is added to identify the tier membership of swap devices.
Although tier-based allocation logic is not yet implemented, this
mapping is necessary to track which tier a device belongs to. Upon
activation, the device is assigned to a tier by matching its priority
against the configured tier ranges.
The infrastructure allows dynamic modification of tiers, such as
splitting or merging ranges. These operations are permitted provided
that the tier assignment of already configured swap devices remains
unchanged.
This patch also adds the documentation for the swap tier feature,
covering the core concepts, sysfs interface usage, and configuration
details.
Reviewed-by: Baoquan He <baoquan.he@linux.dev>
Signed-off-by: Youngjun Park <youngjun.park@lge.com>
---
Documentation/mm/index.rst | 1 +
Documentation/mm/swap-tier.rst | 150 +++++++++++++++++++++++++++++++++
MAINTAINERS | 1 +
include/linux/swap.h | 1 +
mm/swap_state.c | 2 +-
mm/swap_tier.c | 101 +++++++++++++++++++---
mm/swap_tier.h | 13 ++-
mm/swapfile.c | 2 +
8 files changed, 257 insertions(+), 14 deletions(-)
create mode 100644 Documentation/mm/swap-tier.rst
diff --git a/Documentation/mm/index.rst b/Documentation/mm/index.rst
index 13a79f5d092c..6afc45cd4b3d 100644
--- a/Documentation/mm/index.rst
+++ b/Documentation/mm/index.rst
@@ -34,6 +34,7 @@ see the :doc:`admin guide <../admin-guide/mm/index>`.
page_reclaim
swap
swap-table
+ swap-tier
page_cache
shmfs
oom
diff --git a/Documentation/mm/swap-tier.rst b/Documentation/mm/swap-tier.rst
new file mode 100644
index 000000000000..0fb4a1153a67
--- /dev/null
+++ b/Documentation/mm/swap-tier.rst
@@ -0,0 +1,150 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+:Author: Chris Li <chrisl@kernel.org> Youngjun Park <youngjun.park@lge.com>
+
+==========
+Swap Tier
+==========
+
+Swap tier is a collection of user-named groups classified by priority ranges.
+It acts as a facilitation layer, allowing users to manage swap devices based
+on their speeds.
+
+Users are encouraged to assign swap device priorities according to device
+speed to fully utilize this feature. While the current implementation is
+integrated with cgroups, the concept is designed to be extensible for other
+subsystems in the future.
+
+Priority Range
+--------------
+
+The specified tiers must cover the entire priority range from -1
+(DEF_SWAP_PRIO) to SHRT_MAX.
+
+Consistency
+-----------
+
+Tier consistency is guaranteed with a focus on maximizing flexibility. When a
+swap device is activated within a tier range, the tier covering that device's
+priority is guaranteed not to disappear or change while the device remains
+active. Adding a new tier may split the range of an existing tier, but the
+active device's tier assignment remains unchanged.
+
+However, specifying a tier in a cgroup does not guarantee the tier's existence.
+Consequently, the corresponding tier can disappear at any time.
+
+Configuration Interface
+-----------------------
+
+The swap tiers can be configured via the following interface:
+
+/sys/kernel/mm/swap/tiers
+
+Operations can be performed using the following syntax:
+
+* Add: ``+"<tiername>":"<start_priority>"``
+* Remove: ``-"<tiername>"``
+
+Tier names must consist of alphanumeric characters and underscores. Multiple
+operations can be provided in a single write, separated by commas (",") or
+whitespace (spaces, tabs, newlines).
+
+When configuring tiers, the specified value represents the **start priority**
+of that tier. The end priority is automatically determined by the start
+priority of the next higher tier. Consequently, adding a tier
+automatically adjusts the ranges of adjacent tiers to ensure continuity.
+
+Examples
+--------
+
+**1. Initialization**
+
+A tier starting at -1 is mandatory to cover the entire priority range up to
+SHRT_MAX. In this example, 'HDD' starts at 50, and 'NET' covers the remaining
+lower range starting from -1.
+
+::
+
+ # echo "+HDD:50, +NET:-1" > /sys/kernel/mm/swap/tiers
+ # cat /sys/kernel/mm/swap/tiers
+ Name Idx PrioStart PrioEnd
+ HDD 0 50 32767
+ NET 1 -1 49
+
+**2. Adding a New Tier (split)**
+
+A new tier 'SSD' is added at priority 100, splitting the existing 'HDD' tier.
+The ranges are automatically recalculated:
+
+* 'SSD' takes the top range (100 to SHRT_MAX).
+* 'HDD' is adjusted to the range between 'NET' and 'SSD' (50 to 99).
+* 'NET' remains unchanged (-1 to 49).
+
+::
+
+ # echo "+SSD:100" > /sys/kernel/mm/swap/tiers
+ # cat /sys/kernel/mm/swap/tiers
+ Name Idx PrioStart PrioEnd
+ SSD 2 100 32767
+ HDD 0 50 99
+ NET 1 -1 49
+
+**3. Removal (merge)**
+
+Tiers can be removed using the '-' prefix.
+::
+
+ # echo "-SSD" > /sys/kernel/mm/swap/tiers
+
+When a tier is removed, its priority range is merged into the adjacent
+tier. The merge direction is always upward (the tier below expands),
+except when the lowest tier is removed — in that case the tier above
+shifts its starting priority down to -1 to maintain full range coverage.
+
+::
+
+ Initial state:
+ Name Idx PrioStart PrioEnd
+ SSD 2 100 32767
+ HDD 1 50 99
+ NET 0 -1 49
+
+ # echo "-SSD" > /sys/kernel/mm/swap/tiers
+
+ Name Idx PrioStart PrioEnd
+ HDD 1 50 32767 <- merged with SSD's range
+ NET 0 -1 49
+
+ # echo "-NET" > /sys/kernel/mm/swap/tiers
+
+ Name Idx PrioStart PrioEnd
+ HDD 1 -1 32767 <- shifted down to -1
+
+**4. Interaction with Active Swap Devices**
+
+If a swap device is active (swapon), the tier covering that device's
+priority cannot be removed. Splitting the active tier's range is only
+allowed above the device's priority.
+
+Assume a swap device is active at priority 60 (inside 'HDD' tier).
+
+::
+
+ # swapon -p 60 /dev/zram0
+
+ Name Idx PrioStart PrioEnd
+ HDD 0 50 32767
+ NET 1 -1 49
+
+ # echo "-HDD" > /sys/kernel/mm/swap/tiers
+ -bash: echo: write error: Device or resource busy
+
+ # echo "+SSD:60" > /sys/kernel/mm/swap/tiers
+ -bash: echo: write error: Device or resource busy
+
+ # echo "+SSD:100" > /sys/kernel/mm/swap/tiers
+
+ Name Idx PrioStart PrioEnd
+ SSD 2 100 32767
+ HDD 0 50 99 <- device (prio 60) stays here
+ NET 1 -1 49
diff --git a/MAINTAINERS b/MAINTAINERS
index e94d1af17c39..4485a0650984 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -17237,6 +17237,7 @@ L: linux-mm@kvack.org
S: Maintained
F: Documentation/ABI/testing/sysfs-kernel-mm-swap
F: Documentation/mm/swap-table.rst
+F: Documentation/mm/swap-tier.rst
F: include/linux/swap.h
F: include/linux/swapfile.h
F: include/linux/swapops.h
diff --git a/include/linux/swap.h b/include/linux/swap.h
index 4427c7aa16dc..2d647a8fa60f 100644
--- a/include/linux/swap.h
+++ b/include/linux/swap.h
@@ -244,6 +244,7 @@ struct swap_info_struct {
struct percpu_ref users; /* indicate and keep swap device valid. */
unsigned long flags; /* SWP_USED etc: see above */
signed short prio; /* swap priority of this type */
+ int tier_mask; /* swap tier mask */
struct plist_node list; /* entry in swap_active_head */
signed char type; /* strange name for an index */
unsigned int max; /* size of this swap device */
diff --git a/mm/swap_state.c b/mm/swap_state.c
index c18ff741f2e0..dcec1a6c92bd 100644
--- a/mm/swap_state.c
+++ b/mm/swap_state.c
@@ -1095,7 +1095,7 @@ static ssize_t tiers_store(struct kobject *kobj,
}
}
- if (!swap_tiers_validate()) {
+ if (!swap_tiers_update()) {
ret = -EINVAL;
goto restore;
}
diff --git a/mm/swap_tier.c b/mm/swap_tier.c
index ac7a3c2a48cb..6b57cadb3e95 100644
--- a/mm/swap_tier.c
+++ b/mm/swap_tier.c
@@ -38,6 +38,8 @@ static LIST_HEAD(swap_tier_inactive_list);
(!list_is_first(&(tier)->list, &swap_tier_active_list) ? \
list_prev_entry((tier), list)->prio - 1 : SHRT_MAX)
+#define MASK_TO_TIER(mask) (&swap_tiers[__ffs((mask))])
+
#define for_each_tier(tier, idx) \
for (idx = 0, tier = &swap_tiers[0]; idx < MAX_SWAPTIER; \
idx++, tier = &swap_tiers[idx])
@@ -59,6 +61,26 @@ static bool swap_tier_is_active(void)
return !list_empty(&swap_tier_active_list);
}
+static bool swap_tier_prio_in_range(struct swap_tier *tier, short prio)
+{
+ if (tier->prio <= prio && TIER_END_PRIO(tier) >= prio)
+ return true;
+
+ return false;
+}
+
+static bool swap_tier_prio_is_used(short prio)
+{
+ struct swap_tier *tier;
+
+ for_each_active_tier(tier) {
+ if (tier->prio == prio)
+ return true;
+ }
+
+ return false;
+}
+
static struct swap_tier *swap_tier_lookup(const char *name)
{
struct swap_tier *tier;
@@ -99,6 +121,7 @@ void swap_tiers_init(void)
int idx;
BUILD_BUG_ON(BITS_PER_TYPE(int) < MAX_SWAPTIER);
+ BUILD_BUG_ON(MAX_SWAPTIER > TIER_DEFAULT_IDX);
for_each_tier(tier, idx) {
INIT_LIST_HEAD(&tier->list);
@@ -149,17 +172,29 @@ static struct swap_tier *swap_tier_prepare(const char *name, short prio)
return tier;
}
-static int swap_tier_check_range(short prio)
+static int swap_tier_can_split_range(short new_prio)
{
+ struct swap_info_struct *p;
struct swap_tier *tier;
lockdep_assert_held(&swap_lock);
lockdep_assert_held(&swap_tier_lock);
- for_each_active_tier(tier) {
- /* No overwrite */
- if (tier->prio == prio)
- return -EINVAL;
+ plist_for_each_entry(p, &swap_active_head, list) {
+ if (p->tier_mask == TIER_DEFAULT_MASK)
+ continue;
+
+ tier = MASK_TO_TIER(p->tier_mask);
+ if (!swap_tier_prio_in_range(tier, new_prio))
+ continue;
+
+ /*
+ * Device sits in a tier that spans new_prio;
+ * splitting here would reassign it to a
+ * different tier.
+ */
+ if (p->prio >= new_prio)
+ return -EBUSY;
}
return 0;
@@ -199,7 +234,11 @@ int swap_tiers_add(const char *name, int prio)
if (!swap_tier_validate_name(name))
return -EINVAL;
- ret = swap_tier_check_range(prio);
+ /* No overwrite */
+ if (swap_tier_prio_is_used(prio))
+ return -EBUSY;
+
+ ret = swap_tier_can_split_range(prio);
if (ret)
return ret;
@@ -226,6 +265,11 @@ int swap_tiers_remove(const char *name)
if (!tier)
return -EINVAL;
+ /* Simulate adding a tier to check for conflicts */
+ ret = swap_tier_can_split_range(tier->prio);
+ if (ret)
+ return ret;
+
/* Removing DEF_SWAP_PRIO merges into the higher tier. */
if (!list_is_singular(&swap_tier_active_list)
&& tier->prio == DEF_SWAP_PRIO)
@@ -236,13 +280,15 @@ int swap_tiers_remove(const char *name)
return ret;
}
-static struct swap_tier swap_tiers_snap[MAX_SWAPTIER];
/*
- * XXX: When multiple operations (adds and removes) are submitted in a
- * single write, reverting each individually on failure is complex and
- * error-prone. Instead, snapshot the entire state beforehand and
- * restore it wholesale if any operation fails.
+ * XXX: Static global snapshot buffer for batch operations. Small
+ * and used once per write, so a static global is not bad.
+ * When multiple adds/removes are submitted in a single write,
+ * reverting each individually on failure is error-prone. Instead,
+ * snapshot beforehand and restore wholesale if any operation fails.
*/
+static struct swap_tier swap_tiers_snap[MAX_SWAPTIER];
+
void swap_tiers_snapshot(void)
{
BUILD_BUG_ON(sizeof(swap_tiers_snap) != sizeof(swap_tiers));
@@ -282,10 +328,30 @@ void swap_tiers_snapshot_restore(void)
}
}
-bool swap_tiers_validate(void)
+void swap_tiers_assign_dev(struct swap_info_struct *swp)
{
struct swap_tier *tier;
+ lockdep_assert_held(&swap_lock);
+
+ for_each_active_tier(tier) {
+ if (swap_tier_prio_in_range(tier, swp->prio)) {
+ swp->tier_mask = TIER_MASK(tier);
+ return;
+ }
+ }
+
+ swp->tier_mask = TIER_DEFAULT_MASK;
+}
+
+bool swap_tiers_update(void)
+{
+ struct swap_tier *tier;
+ struct swap_info_struct *swp;
+
+ lockdep_assert_held(&swap_lock);
+ lockdep_assert_held(&swap_tier_lock);
+
/*
* Initial setting might not cover DEF_SWAP_PRIO.
* Swap tier must cover the full range (DEF_SWAP_PRIO to SHRT_MAX).
@@ -298,5 +364,16 @@ bool swap_tiers_validate(void)
return false;
}
+ /*
+ * If applied initially, the swap tier_mask may change
+ * from the default value.
+ */
+ plist_for_each_entry(swp, &swap_active_head, list) {
+ /* Tier is already configured */
+ if (swp->tier_mask != TIER_DEFAULT_MASK)
+ break;
+ swap_tiers_assign_dev(swp);
+ }
+
return true;
}
diff --git a/mm/swap_tier.h b/mm/swap_tier.h
index a1395ec02c24..3e355f857363 100644
--- a/mm/swap_tier.h
+++ b/mm/swap_tier.h
@@ -5,8 +5,15 @@
#include <linux/types.h>
#include <linux/spinlock.h>
+/* Forward declarations */
+struct swap_info_struct;
+
extern spinlock_t swap_tier_lock;
+#define TIER_ALL_MASK (~0)
+#define TIER_DEFAULT_IDX (31)
+#define TIER_DEFAULT_MASK (1U << TIER_DEFAULT_IDX)
+
/* Initialization and application */
void swap_tiers_init(void);
ssize_t swap_tiers_sysfs_show(char *buf);
@@ -16,5 +23,9 @@ int swap_tiers_remove(const char *name);
void swap_tiers_snapshot(void);
void swap_tiers_snapshot_restore(void);
-bool swap_tiers_validate(void);
+bool swap_tiers_update(void);
+
+/* Tier assignment */
+void swap_tiers_assign_dev(struct swap_info_struct *swp);
+
#endif /* _SWAP_TIER_H */
diff --git a/mm/swapfile.c b/mm/swapfile.c
index ff567ad893a4..f3cff586cf30 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -3041,6 +3041,8 @@ static void _enable_swap_info(struct swap_info_struct *si)
/* Add back to available list */
add_to_avail_list(si, true);
+
+ swap_tiers_assign_dev(si);
}
/*
--
2.34.1
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH v10 3/6] mm: memcontrol: add interface for swap tier selection
2026-07-13 2:56 [PATCH v10 0/6] mm/swap, memcg: Introduce swap tiers for cgroup based swap control Youngjun Park
2026-07-13 2:56 ` [PATCH v10 1/6] mm: swap: introduce swap tier infrastructure Youngjun Park
2026-07-13 2:56 ` [PATCH v10 2/6] mm: swap: associate swap devices with tiers Youngjun Park
@ 2026-07-13 2:56 ` Youngjun Park
2026-07-13 2:56 ` [PATCH v10 4/6] mm: swap: filter swap allocation by memcg tier mask Youngjun Park
` (3 subsequent siblings)
6 siblings, 0 replies; 20+ messages in thread
From: Youngjun Park @ 2026-07-13 2:56 UTC (permalink / raw)
To: akpm
Cc: chrisl, youngjun.park, linux-mm, cgroups, linux-kernel, kasong,
hannes, mhocko, roman.gushchin, shakeel.butt, muchun.song,
shikemeng, baoquan.he, baohua, yosry, joshua.hahnjy, gunho.lee,
taejoon.song, hyungjun.cho, baver.bae, her0gyugyu
Introduce memory.swap.tiers.max, a flat-keyed file listing each
tier defined in /sys/kernel/mm/swap/tiers with its state, "max"
(allowed, the default) or "0" (disabled). A tier is one bit in the
cgroup's tier mask, so writing "<tier> max" or "<tier> 0" sets or
clears that bit.
Since the current use case lacks amount control, it only supports
"max" (on) and "0" (off). Therefore, it does not track per-tier swap
usage, relying instead on a fast runtime bitmask check.
We maintain both `mask` and `effective_mask`. The `effective_mask` is
strictly bounded by the parent (e.g., if a parent is "0", the child's
effective state is "0" even if its `mask` is "max"). Maintaining this
separately avoids costly cgroup tree traversals to check ancestors at
runtime.
Suggested-by: Shakeel Butt <shakeel.butt@linux.dev>
Suggested-by: Yosry Ahmed <yosry@kernel.org>
Signed-off-by: Youngjun Park <youngjun.park@lge.com>
---
Documentation/admin-guide/cgroup-v2.rst | 20 +++++
Documentation/mm/swap-tier.rst | 9 +++
include/linux/memcontrol.h | 5 ++
mm/memcontrol.c | 67 ++++++++++++++++
mm/swap_state.c | 5 +-
mm/swap_tier.c | 102 +++++++++++++++++++++++-
mm/swap_tier.h | 57 +++++++++++--
7 files changed, 255 insertions(+), 10 deletions(-)
diff --git a/Documentation/admin-guide/cgroup-v2.rst b/Documentation/admin-guide/cgroup-v2.rst
index 993446ab66d0..4b0b4f00ad6e 100644
--- a/Documentation/admin-guide/cgroup-v2.rst
+++ b/Documentation/admin-guide/cgroup-v2.rst
@@ -1850,6 +1850,26 @@ The following nested keys are defined.
Swap usage hard limit. If a cgroup's swap usage reaches this
limit, anonymous memory of the cgroup will not be swapped out.
+ memory.swap.tiers.max
+ A read-write flat-keyed file which exists on non-root
+ cgroups. The default is "max" for every tier.
+
+ Limits the swap tiers this cgroup may swap to. Tiers are
+ defined globally in /sys/kernel/mm/swap/tiers and listed here,
+ one per line. When read, the values are displayed in descending
+ order of the tiers (highest tier first)::
+
+ <tier_1> max
+ <tier_2> 0
+ ...
+
+ Currently, only "max" and "0" are supported. "max" allows the
+ tier, "0" disables it. Each write sets a single "<tier> max"
+ or "<tier> 0" pair.
+
+ A child may only narrow what its parent allows. A tier an
+ ancestor disabled stays disabled regardless of the value here.
+
memory.swap.events
A read-only flat-keyed file which exists on non-root cgroups.
The following entries are defined. Unless specified
diff --git a/Documentation/mm/swap-tier.rst b/Documentation/mm/swap-tier.rst
index 0fb4a1153a67..addbc495de8c 100644
--- a/Documentation/mm/swap-tier.rst
+++ b/Documentation/mm/swap-tier.rst
@@ -15,6 +15,15 @@ speed to fully utilize this feature. While the current implementation is
integrated with cgroups, the concept is designed to be extensible for other
subsystems in the future.
+Use case
+---------
+
+Users can perform selective swapping by choosing a swap tier assigned according
+to speed within a cgroup.
+
+For more information on cgroup v2, please refer to
+``Documentation/admin-guide/cgroup-v2.rst``.
+
Priority Range
--------------
diff --git a/include/linux/memcontrol.h b/include/linux/memcontrol.h
index 8724ff417ad4..895da2cc1a69 100644
--- a/include/linux/memcontrol.h
+++ b/include/linux/memcontrol.h
@@ -281,6 +281,11 @@ struct mem_cgroup {
struct lru_gen_mm_list mm_list;
#endif
+#ifdef CONFIG_SWAP
+ int tier_mask;
+ int tier_effective_mask;
+#endif
+
#ifdef CONFIG_MEMCG_V1
/* Legacy consumer-oriented counters */
struct page_counter kmem; /* v1 only */
diff --git a/mm/memcontrol.c b/mm/memcontrol.c
index 929be41cadd4..6403d6b3ca41 100644
--- a/mm/memcontrol.c
+++ b/mm/memcontrol.c
@@ -68,6 +68,7 @@
#include <net/ip.h>
#include "slab.h"
#include "memcontrol-v1.h"
+#include "swap_tier.h"
#include <linux/uaccess.h>
@@ -4244,6 +4245,8 @@ static int mem_cgroup_css_online(struct cgroup_subsys_state *css)
refcount_set(&memcg->id.ref, 1);
css_get(css);
+ swap_tiers_memcg_inherit_mask(memcg);
+
/*
* Ensure mem_cgroup_from_private_id() works once we're fully online.
*
@@ -5798,6 +5801,64 @@ static int swap_events_show(struct seq_file *m, void *v)
return 0;
}
+static int swap_tier_max_show(struct seq_file *m, void *v)
+{
+ struct mem_cgroup *memcg = mem_cgroup_from_seq(m);
+
+ swap_tiers_mask_show(m, memcg);
+ return 0;
+}
+
+static ssize_t swap_tier_max_write(struct kernfs_open_file *of,
+ char *buf, size_t nbytes, loff_t off)
+{
+ struct mem_cgroup *memcg = mem_cgroup_from_css(of_css(of));
+ char *pos, *name, *val;
+ bool enable;
+ int mask;
+ int ret = 0;
+
+ pos = strstrip(buf);
+ name = strsep(&pos, " \t\n");
+ if (!name || !*name)
+ return -EINVAL;
+ if (pos)
+ pos = skip_spaces(pos);
+ val = strsep(&pos, " \t\n");
+ if (!val || !*val)
+ return -EINVAL;
+ if (pos && *skip_spaces(pos))
+ return -EINVAL;
+
+ if (!strcmp(val, "max"))
+ enable = true;
+ else if (!strcmp(val, "0"))
+ enable = false;
+ else
+ return -EINVAL;
+
+ spin_lock(&swap_tier_lock);
+ mask = swap_tiers_mask_lookup(name);
+ if (!mask) {
+ ret = -EINVAL;
+ goto out;
+ }
+
+ /*
+ * tier_mask is set per memcg here; the effective mask is clamped
+ * to the parent's in swap_tiers_memcg_sync_mask().
+ */
+ if (enable)
+ WRITE_ONCE(memcg->tier_mask, memcg->tier_mask | mask);
+ else
+ WRITE_ONCE(memcg->tier_mask, memcg->tier_mask & ~mask);
+
+ swap_tiers_memcg_sync_mask(memcg);
+out:
+ spin_unlock(&swap_tier_lock);
+ return ret ? ret : nbytes;
+}
+
static struct cftype swap_files[] = {
{
.name = "swap.current",
@@ -5830,6 +5891,12 @@ static struct cftype swap_files[] = {
.file_offset = offsetof(struct mem_cgroup, swap_events_file),
.seq_show = swap_events_show,
},
+ {
+ .name = "swap.tiers.max",
+ .flags = CFTYPE_NOT_ON_ROOT,
+ .seq_show = swap_tier_max_show,
+ .write = swap_tier_max_write,
+ },
{ } /* terminate */
};
diff --git a/mm/swap_state.c b/mm/swap_state.c
index dcec1a6c92bd..ba090fa80bf1 100644
--- a/mm/swap_state.c
+++ b/mm/swap_state.c
@@ -1053,6 +1053,7 @@ static ssize_t tiers_store(struct kobject *kobj,
char *p, *token, *name, *tmp;
int ret = 0;
short prio;
+ int mask = 0;
tmp = kstrdup(buf, GFP_KERNEL);
if (!tmp)
@@ -1085,7 +1086,7 @@ static ssize_t tiers_store(struct kobject *kobj,
goto restore;
break;
case '-':
- ret = swap_tiers_remove(token + 1);
+ ret = swap_tiers_remove(token + 1, &mask);
if (ret)
goto restore;
break;
@@ -1095,7 +1096,7 @@ static ssize_t tiers_store(struct kobject *kobj,
}
}
- if (!swap_tiers_update()) {
+ if (!swap_tiers_update(mask)) {
ret = -EINVAL;
goto restore;
}
diff --git a/mm/swap_tier.c b/mm/swap_tier.c
index 6b57cadb3e95..98bfee760b8d 100644
--- a/mm/swap_tier.c
+++ b/mm/swap_tier.c
@@ -253,7 +253,7 @@ int swap_tiers_add(const char *name, int prio)
return ret;
}
-int swap_tiers_remove(const char *name)
+int swap_tiers_remove(const char *name, int *mask)
{
int ret = 0;
struct swap_tier *tier;
@@ -276,6 +276,7 @@ int swap_tiers_remove(const char *name)
list_prev_entry(tier, list)->prio = DEF_SWAP_PRIO;
swap_tier_inactivate(tier);
+ *mask |= TIER_MASK(tier);
return ret;
}
@@ -344,7 +345,24 @@ void swap_tiers_assign_dev(struct swap_info_struct *swp)
swp->tier_mask = TIER_DEFAULT_MASK;
}
-bool swap_tiers_update(void)
+#ifdef CONFIG_MEMCG
+static void swap_tier_memcg_propagate(int mask)
+{
+ struct mem_cgroup *child;
+
+ for_each_mem_cgroup_tree(child, root_mem_cgroup) {
+ WRITE_ONCE(child->tier_mask, child->tier_mask | mask);
+ WRITE_ONCE(child->tier_effective_mask,
+ child->tier_effective_mask | mask);
+ }
+}
+#else
+static void swap_tier_memcg_propagate(int mask)
+{
+}
+#endif
+
+bool swap_tiers_update(int mask)
{
struct swap_tier *tier;
struct swap_info_struct *swp;
@@ -375,5 +393,85 @@ bool swap_tiers_update(void)
swap_tiers_assign_dev(swp);
}
+ /*
+ * When a tier is removed, its index (bit position in the mask) becomes
+ * free for reassignment to a future tier. If a memcg had previously
+ * disabled this tier (cleared the bit in its swap.tiers.max file), the
+ * effective mask would keep that bit clear -- meaning the new tier at
+ * the same index would be silently unavailable, an invisible cgroup
+ * constraint left behind by a tier that no longer exists.
+ *
+ * To prevent this, OR the removed tier's mask bit into every memcg's
+ * tier_mask and tier_effective_mask. This resets the bit so the new
+ * tier is accessible by default; users who want to restrict it must
+ * explicitly disable it after the tier is re-created.
+ */
+ if (mask)
+ swap_tier_memcg_propagate(mask);
+
return true;
}
+
+#ifdef CONFIG_MEMCG
+void swap_tiers_mask_show(struct seq_file *m, struct mem_cgroup *memcg)
+{
+ struct swap_tier *tier;
+ int mask;
+
+ spin_lock(&swap_tier_lock);
+ mask = READ_ONCE(memcg->tier_mask);
+
+ for_each_active_tier(tier)
+ seq_printf(m, "%s %s\n", tier->name,
+ (mask & TIER_MASK(tier)) ? "max" : "0");
+ spin_unlock(&swap_tier_lock);
+}
+
+int swap_tiers_mask_lookup(const char *name)
+{
+ struct swap_tier *tier;
+
+ lockdep_assert_held(&swap_tier_lock);
+
+ for_each_active_tier(tier) {
+ if (!strcmp(name, tier->name))
+ return TIER_MASK(tier);
+ }
+
+ return 0;
+}
+
+static void __swap_tier_memcg_inherit_mask(struct mem_cgroup *memcg,
+ struct mem_cgroup *parent)
+{
+ int parent_mask = parent
+ ? READ_ONCE(parent->tier_effective_mask)
+ : TIER_ALL_MASK;
+
+ WRITE_ONCE(memcg->tier_effective_mask,
+ parent_mask & READ_ONCE(memcg->tier_mask));
+}
+
+/* Computes the initial effective mask from the parent's effective mask. */
+void swap_tiers_memcg_inherit_mask(struct mem_cgroup *memcg)
+{
+ spin_lock(&swap_tier_lock);
+ memcg->tier_mask = TIER_ALL_MASK;
+ __swap_tier_memcg_inherit_mask(memcg, parent_mem_cgroup(memcg));
+ spin_unlock(&swap_tier_lock);
+}
+
+/*
+ * Called when a memcg's tier_mask is modified. Walks the subtree
+ * and recomputes each descendant's effective mask against its parent.
+ */
+void swap_tiers_memcg_sync_mask(struct mem_cgroup *memcg)
+{
+ struct mem_cgroup *child;
+
+ lockdep_assert_held(&swap_tier_lock);
+
+ for_each_mem_cgroup_tree(child, memcg)
+ __swap_tier_memcg_inherit_mask(child, parent_mem_cgroup(child));
+}
+#endif
diff --git a/mm/swap_tier.h b/mm/swap_tier.h
index 3e355f857363..e2f0cf32035b 100644
--- a/mm/swap_tier.h
+++ b/mm/swap_tier.h
@@ -10,22 +10,67 @@ struct swap_info_struct;
extern spinlock_t swap_tier_lock;
-#define TIER_ALL_MASK (~0)
-#define TIER_DEFAULT_IDX (31)
-#define TIER_DEFAULT_MASK (1U << TIER_DEFAULT_IDX)
-
/* Initialization and application */
void swap_tiers_init(void);
ssize_t swap_tiers_sysfs_show(char *buf);
int swap_tiers_add(const char *name, int prio);
-int swap_tiers_remove(const char *name);
+int swap_tiers_remove(const char *name, int *mask);
void swap_tiers_snapshot(void);
void swap_tiers_snapshot_restore(void);
-bool swap_tiers_update(void);
+bool swap_tiers_update(int mask);
/* Tier assignment */
void swap_tiers_assign_dev(struct swap_info_struct *swp);
+#define TIER_ALL_MASK (~0)
+#define TIER_DEFAULT_IDX (31)
+#define TIER_DEFAULT_MASK (1U << TIER_DEFAULT_IDX)
+
+#if defined(CONFIG_SWAP) && defined(CONFIG_MEMCG)
+/* Memcg related functions */
+void swap_tiers_mask_show(struct seq_file *m, struct mem_cgroup *memcg);
+void swap_tiers_memcg_inherit_mask(struct mem_cgroup *memcg);
+void swap_tiers_memcg_sync_mask(struct mem_cgroup *memcg);
+int swap_tiers_mask_lookup(const char *name);
+static inline int folio_tier_effective_mask(struct folio *folio)
+{
+ struct mem_cgroup *memcg;
+ int mask = TIER_ALL_MASK;
+
+ rcu_read_lock();
+ memcg = folio_memcg(folio);
+ if (memcg)
+ mask = READ_ONCE(memcg->tier_effective_mask);
+ rcu_read_unlock();
+
+ return mask;
+}
+#else
+static inline void swap_tiers_mask_show(struct seq_file *m,
+ struct mem_cgroup *memcg) {}
+static inline void swap_tiers_memcg_inherit_mask(struct mem_cgroup *memcg) {}
+static inline void swap_tiers_memcg_sync_mask(struct mem_cgroup *memcg) {}
+static inline int swap_tiers_mask_lookup(const char *name)
+{
+ return 0;
+}
+static inline int folio_tier_effective_mask(struct folio *folio)
+{
+ return TIER_ALL_MASK;
+}
+#endif
+
+/**
+ * swap_tiers_mask_test - Check if the tier mask is valid
+ * @tier_mask: The tier mask to check
+ * @mask: The mask to compare against
+ *
+ * Return: true if condition matches, false otherwise
+ */
+static inline bool swap_tiers_mask_test(int tier_mask, int mask)
+{
+ return tier_mask & mask;
+}
#endif /* _SWAP_TIER_H */
--
2.34.1
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH v10 4/6] mm: swap: filter swap allocation by memcg tier mask
2026-07-13 2:56 [PATCH v10 0/6] mm/swap, memcg: Introduce swap tiers for cgroup based swap control Youngjun Park
` (2 preceding siblings ...)
2026-07-13 2:56 ` [PATCH v10 3/6] mm: memcontrol: add interface for swap tier selection Youngjun Park
@ 2026-07-13 2:56 ` Youngjun Park
2026-07-13 2:56 ` [PATCH v10 5/6] selftests/mm: add a swap tier configuration test Youngjun Park
` (2 subsequent siblings)
6 siblings, 0 replies; 20+ messages in thread
From: Youngjun Park @ 2026-07-13 2:56 UTC (permalink / raw)
To: akpm
Cc: chrisl, youngjun.park, linux-mm, cgroups, linux-kernel, kasong,
hannes, mhocko, roman.gushchin, shakeel.butt, muchun.song,
shikemeng, baoquan.he, baohua, yosry, joshua.hahnjy, gunho.lee,
taejoon.song, hyungjun.cho, baver.bae, her0gyugyu
Apply memcg tier effective mask during swap slot allocation to
enforce per-cgroup swap tier restrictions.
The folio's effective mask is computed once and passed to the fast,
slow and discard paths as a parameter, so all of them act on the same
mask even if the memcg's mask changes concurrently.
In the fast path, check the percpu cached swap_info's tier_mask
against the folio's effective mask. If it does not match, fall
through to the slow path. In the slow path, skip swap devices
whose tier_mask is not covered by the folio's effective mask.
The discard fallback honors the mask too: otherwise it would drain
the discard clusters of a device outside the folio's tiers and then
loop back to allocate from a tier the memcg is not allowed to use.
This works correctly when there is only one non-rotational
device in the system and no devices share the same priority.
However, there are known limitations:
- When non-rotational devices are distributed across multiple
tiers, and different memcgs are configured to use those
distinct tiers, they may constantly overwrite the shared
percpu swap cache. This cache thrashing leads to frequent
fast path misses.
- Combined with the above issue, if same-priority devices exist
among them, a percpu cache miss (overwritten by another memcg)
forces the allocator to round-robin to the next device
prematurely, even if the current cluster is not fully
exhausted.
These edge cases do not affect the primary use case of
directing swap traffic per cgroup. Further optimization is
planned for future work.
Signed-off-by: Youngjun Park <youngjun.park@lge.com>
---
mm/swapfile.c | 24 +++++++++++++++++-------
1 file changed, 17 insertions(+), 7 deletions(-)
diff --git a/mm/swapfile.c b/mm/swapfile.c
index f3cff586cf30..967399936108 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -1359,7 +1359,7 @@ static bool get_swap_device_info(struct swap_info_struct *si)
* Fast path try to get swap entries with specified order from current
* CPU's swap entry pool (a cluster).
*/
-static bool swap_alloc_fast(struct folio *folio)
+static bool swap_alloc_fast(struct folio *folio, int mask)
{
unsigned int order = folio_order(folio);
struct swap_cluster_info *ci;
@@ -1371,8 +1371,11 @@ static bool swap_alloc_fast(struct folio *folio)
* so checking it's liveness by get_swap_device_info is enough.
*/
si = this_cpu_read(percpu_swap_cluster.si[order]);
+ if (!si || !swap_tiers_mask_test(si->tier_mask, mask))
+ return false;
+
offset = this_cpu_read(percpu_swap_cluster.offset[order]);
- if (!si || !offset || !get_swap_device_info(si))
+ if (!offset || !get_swap_device_info(si))
return false;
ci = swap_cluster_lock(si, offset);
@@ -1389,13 +1392,16 @@ static bool swap_alloc_fast(struct folio *folio)
}
/* Rotate the device and switch to a new cluster */
-static void swap_alloc_slow(struct folio *folio)
+static void swap_alloc_slow(struct folio *folio, int mask)
{
struct swap_info_struct *si, *next;
spin_lock(&swap_avail_lock);
start_over:
plist_for_each_entry_safe(si, next, &swap_avail_head, avail_list) {
+ if (!swap_tiers_mask_test(si->tier_mask, mask))
+ continue;
+
/* Rotate the device and switch to a new cluster */
plist_requeue(&si->avail_list, &swap_avail_head);
spin_unlock(&swap_avail_lock);
@@ -1429,7 +1435,7 @@ static void swap_alloc_slow(struct folio *folio)
* Discard pending clusters in a synchronized way when under high pressure.
* Return: true if any cluster is discarded.
*/
-static bool swap_sync_discard(void)
+static bool swap_sync_discard(int mask)
{
bool ret = false;
struct swap_info_struct *si, *next;
@@ -1437,6 +1443,8 @@ static bool swap_sync_discard(void)
spin_lock(&swap_lock);
start_over:
plist_for_each_entry_safe(si, next, &swap_active_head, list) {
+ if (!swap_tiers_mask_test(si->tier_mask, mask))
+ continue;
spin_unlock(&swap_lock);
if (get_swap_device_info(si)) {
if (si->flags & SWP_PAGE_DISCARD)
@@ -1736,6 +1744,7 @@ int folio_alloc_swap(struct folio *folio)
{
unsigned int order = folio_order(folio);
unsigned int size = 1 << order;
+ int mask;
VM_BUG_ON_FOLIO(!folio_test_locked(folio), folio);
VM_BUG_ON_FOLIO(!folio_test_uptodate(folio), folio);
@@ -1759,13 +1768,14 @@ int folio_alloc_swap(struct folio *folio)
}
again:
+ mask = folio_tier_effective_mask(folio);
local_lock(&percpu_swap_cluster.lock);
- if (!swap_alloc_fast(folio))
- swap_alloc_slow(folio);
+ if (!swap_alloc_fast(folio, mask))
+ swap_alloc_slow(folio, mask);
local_unlock(&percpu_swap_cluster.lock);
if (!order && unlikely(!folio_test_swapcache(folio))) {
- if (swap_sync_discard())
+ if (swap_sync_discard(mask))
goto again;
}
--
2.34.1
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH v10 5/6] selftests/mm: add a swap tier configuration test
2026-07-13 2:56 [PATCH v10 0/6] mm/swap, memcg: Introduce swap tiers for cgroup based swap control Youngjun Park
` (3 preceding siblings ...)
2026-07-13 2:56 ` [PATCH v10 4/6] mm: swap: filter swap allocation by memcg tier mask Youngjun Park
@ 2026-07-13 2:56 ` Youngjun Park
2026-07-13 2:56 ` [PATCH v10 6/6] selftests/cgroup: add a swap tier routing test Youngjun Park
2026-07-13 15:50 ` [PATCH v10 0/6] mm/swap, memcg: Introduce swap tiers for cgroup based swap control Yosry Ahmed
6 siblings, 0 replies; 20+ messages in thread
From: Youngjun Park @ 2026-07-13 2:56 UTC (permalink / raw)
To: akpm
Cc: chrisl, youngjun.park, linux-mm, cgroups, linux-kernel, kasong,
hannes, mhocko, roman.gushchin, shakeel.butt, muchun.song,
shikemeng, baoquan.he, baohua, yosry, joshua.hahnjy, gunho.lee,
taejoon.song, hyungjun.cho, baver.bae, her0gyugyu
This commit adds a test program for the global swap tier interface at
/sys/kernel/mm/swap/tiers. It checks the add, split and remove
operations and the documented error and batch atomicity rules. It also
checks that a tier with an active swap device cannot be removed until
the device is swapped off. That device is a zram device, and the check
is skipped when zram is not available.
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Youngjun Park <youngjun.park@lge.com>
---
tools/testing/selftests/mm/.gitignore | 1 +
tools/testing/selftests/mm/Makefile | 1 +
tools/testing/selftests/mm/config | 2 +
tools/testing/selftests/mm/run_vmtests.sh | 5 +
tools/testing/selftests/mm/swap_tier.c | 337 ++++++++++++++++++++++
5 files changed, 346 insertions(+)
create mode 100644 tools/testing/selftests/mm/swap_tier.c
diff --git a/tools/testing/selftests/mm/.gitignore b/tools/testing/selftests/mm/.gitignore
index 227476d8ff1f..94ee61a9914d 100644
--- a/tools/testing/selftests/mm/.gitignore
+++ b/tools/testing/selftests/mm/.gitignore
@@ -44,6 +44,7 @@ hmm-tests
memfd_secret
soft-dirty
split_huge_page_test
+swap_tier
ksm_tests
local_config.h
local_config.mk
diff --git a/tools/testing/selftests/mm/Makefile b/tools/testing/selftests/mm/Makefile
index ee8def9b4c31..4e47191e06ff 100644
--- a/tools/testing/selftests/mm/Makefile
+++ b/tools/testing/selftests/mm/Makefile
@@ -104,6 +104,7 @@ TEST_GEN_FILES += guard-regions
TEST_GEN_FILES += merge
TEST_GEN_FILES += rmap
TEST_GEN_FILES += folio_split_race_test
+TEST_GEN_FILES += swap_tier
ifneq ($(ARCH),arm64)
TEST_GEN_FILES += soft-dirty
diff --git a/tools/testing/selftests/mm/config b/tools/testing/selftests/mm/config
index 06f78bd232e2..de3752e1bbd2 100644
--- a/tools/testing/selftests/mm/config
+++ b/tools/testing/selftests/mm/config
@@ -14,3 +14,5 @@ CONFIG_UPROBES=y
CONFIG_MEMORY_FAILURE=y
CONFIG_HWPOISON_INJECT=m
CONFIG_PROC_MEM_ALWAYS_FORCE=y
+CONFIG_SWAP=y
+CONFIG_ZRAM=y
diff --git a/tools/testing/selftests/mm/run_vmtests.sh b/tools/testing/selftests/mm/run_vmtests.sh
index a60b9f9f16e7..06d53fad82ba 100755
--- a/tools/testing/selftests/mm/run_vmtests.sh
+++ b/tools/testing/selftests/mm/run_vmtests.sh
@@ -71,6 +71,8 @@ separated by spaces:
tests for VM_PFNMAP handling
- process_madv
test for process_madv
+- swap_tier
+ test the /sys/kernel/mm/swap/tiers configuration interface
- cow
test copy-on-write semantics
- thp
@@ -352,6 +354,9 @@ CATEGORY="process_madv" run_test ./process_madv
CATEGORY="vma_merge" run_test ./merge
+# swap tier configuration interface (/sys/kernel/mm/swap/tiers)
+CATEGORY="swap_tier" run_test ./swap_tier
+
if [ -x ./memfd_secret ]
then
if [ -f /proc/sys/kernel/yama/ptrace_scope ]; then
diff --git a/tools/testing/selftests/mm/swap_tier.c b/tools/testing/selftests/mm/swap_tier.c
new file mode 100644
index 000000000000..bc77b59a7270
--- /dev/null
+++ b/tools/testing/selftests/mm/swap_tier.c
@@ -0,0 +1,337 @@
+// SPDX-License-Identifier: GPL-2.0
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/swap.h>
+#include <unistd.h>
+
+#include "kselftest.h"
+
+#define TIERS_PATH "/sys/kernel/mm/swap/tiers"
+
+static int tiers_write(const char *cmd)
+{
+ int fd, ret = 0;
+
+ fd = open(TIERS_PATH, O_WRONLY);
+ if (fd < 0)
+ return -errno;
+ if (write(fd, cmd, strlen(cmd)) < 0)
+ ret = -errno;
+ close(fd);
+ return ret;
+}
+
+static int tier_range(const char *name, int *start, int *end)
+{
+ char buf[4096], *line, *save;
+ int fd;
+ ssize_t n;
+
+ fd = open(TIERS_PATH, O_RDONLY);
+ if (fd < 0)
+ return -1;
+ n = read(fd, buf, sizeof(buf) - 1);
+ close(fd);
+ if (n < 0)
+ return -1;
+ buf[n] = '\0';
+
+ for (line = strtok_r(buf, "\n", &save); line;
+ line = strtok_r(NULL, "\n", &save)) {
+ char tname[64];
+ int idx, s, e;
+
+ if (sscanf(line, "%63s %d %d %d", tname, &idx, &s, &e) != 4)
+ continue;
+ if (!strcmp(tname, name)) {
+ *start = s;
+ *end = e;
+ return 0;
+ }
+ }
+ return -1;
+}
+
+static bool tier_exists(const char *name)
+{
+ int s, e;
+
+ return tier_range(name, &s, &e) == 0;
+}
+
+static bool range_is(const char *name, int start, int end)
+{
+ int s, e;
+
+ if (tier_range(name, &s, &e))
+ return false;
+ return s == start && e == end;
+}
+
+static int tier_count(void)
+{
+ char buf[4096], *line, *save;
+ int fd, count = 0;
+ ssize_t n;
+
+ fd = open(TIERS_PATH, O_RDONLY);
+ if (fd < 0)
+ return -1;
+ n = read(fd, buf, sizeof(buf) - 1);
+ close(fd);
+ if (n < 0)
+ return -1;
+ buf[n] = '\0';
+
+ for (line = strtok_r(buf, "\n", &save); line;
+ line = strtok_r(NULL, "\n", &save)) {
+ char tname[64];
+ int idx, s, e;
+
+ if (sscanf(line, "%63s %d %d %d", tname, &idx, &s, &e) == 4)
+ count++;
+ }
+ return count;
+}
+
+static int test_coverage(void)
+{
+ if (tiers_write("+orphan:100") != -EINVAL)
+ return KSFT_FAIL;
+ if (tier_exists("orphan"))
+ return KSFT_FAIL;
+ return KSFT_PASS;
+}
+
+static int test_add(void)
+{
+ if (tiers_write("+lo:-1 +hi:50"))
+ return KSFT_FAIL;
+ if (!range_is("hi", 50, SHRT_MAX) || !range_is("lo", -1, 49))
+ return KSFT_FAIL;
+ return KSFT_PASS;
+}
+
+static int test_split(void)
+{
+ if (tiers_write("+mid:100"))
+ return KSFT_FAIL;
+ if (!range_is("mid", 100, SHRT_MAX) ||
+ !range_is("hi", 50, 99) ||
+ !range_is("lo", -1, 49))
+ return KSFT_FAIL;
+ return KSFT_PASS;
+}
+
+static int test_remove(void)
+{
+ /* Remove the top tier: 'hi' re-expands upward to SHRT_MAX. */
+ if (tiers_write("-mid"))
+ return KSFT_FAIL;
+ if (tier_exists("mid") || !range_is("hi", 50, SHRT_MAX))
+ return KSFT_FAIL;
+
+ /* Remove the lowest tier: 'hi' shifts its start down to -1. */
+ if (tiers_write("-lo"))
+ return KSFT_FAIL;
+ if (tier_exists("lo") || !range_is("hi", -1, SHRT_MAX))
+ return KSFT_FAIL;
+
+ return KSFT_PASS;
+}
+
+static int test_errors(void)
+{
+ if (tiers_write("+hi:100") != -EEXIST) /* duplicate name */
+ return KSFT_FAIL;
+ if (tiers_write("+bad.name:100") != -EINVAL) /* illegal name */
+ return KSFT_FAIL;
+ if (tiers_write("+dup:-1") != -EBUSY) /* priority in use */
+ return KSFT_FAIL;
+ if (tiers_write("+low:-2") != -EINVAL) /* prio < DEF_SWAP_PRIO */
+ return KSFT_FAIL;
+ return KSFT_PASS;
+}
+
+/*
+ * A write carrying several operations is atomic: if any operation fails, the
+ * whole batch is rolled back.
+ */
+static int test_atomic(void)
+{
+ if (tiers_write("+a:50 +a:60") != -EEXIST)
+ return KSFT_FAIL;
+ if (tier_exists("a") || !range_is("hi", -1, SHRT_MAX))
+ return KSFT_FAIL;
+ return KSFT_PASS;
+}
+
+static void zram_remove(int idx);
+static int zram_add(long size)
+{
+ char path[128], val[64];
+ ssize_t n;
+ int idx, fd;
+
+ fd = open("/sys/class/zram-control/hot_add", O_RDONLY);
+ if (fd < 0)
+ return -1;
+ n = read(fd, val, sizeof(val) - 1);
+ close(fd);
+ if (n <= 0)
+ return -1;
+ val[n] = '\0';
+ idx = atoi(val);
+
+ snprintf(path, sizeof(path), "/sys/block/zram%d/disksize", idx);
+ fd = open(path, O_WRONLY);
+ if (fd < 0) {
+ zram_remove(idx);
+ return -1;
+ }
+
+ snprintf(val, sizeof(val), "%ld", size);
+ n = write(fd, val, strlen(val));
+ close(fd);
+
+ if (n != strlen(val)) {
+ zram_remove(idx);
+ return -1;
+ }
+
+ return idx;
+}
+
+static void zram_remove(int idx)
+{
+ char val[16];
+ int fd;
+
+ fd = open("/sys/class/zram-control/hot_remove", O_WRONLY);
+ if (fd < 0)
+ return;
+ snprintf(val, sizeof(val), "%d", idx);
+ write(fd, val, strlen(val)); /* ignore error. best-effort cleanup */
+ close(fd);
+}
+
+static int swap_setup(const char *dev, int prio)
+{
+ char cmd[128];
+
+ snprintf(cmd, sizeof(cmd), "mkswap %s >/dev/null 2>&1", dev);
+ if (system(cmd))
+ return -1;
+ return swapon(dev, SWAP_FLAG_PREFER | (prio & SWAP_FLAG_PRIO_MASK));
+}
+
+static int wait_for_dev(const char *dev)
+{
+ int i;
+
+ for (i = 0; i < 50; i++) {
+ if (access(dev, F_OK) == 0)
+ return 0;
+
+ usleep(100000);
+ }
+
+ return -1;
+}
+
+static int test_device_pins_tier(void)
+{
+ char dev[32];
+ int zidx, ret = KSFT_FAIL;
+
+ if (tiers_write("+top:50"))
+ return KSFT_FAIL;
+
+ zidx = zram_add(64 << 20);
+ if (zidx < 0) {
+ ret = KSFT_SKIP;
+ goto out_tier;
+ }
+ snprintf(dev, sizeof(dev), "/dev/zram%d", zidx);
+
+ if (wait_for_dev(dev)) {
+ ret = KSFT_SKIP;
+ goto out_zram;
+ }
+
+ if (swap_setup(dev, 50)) {
+ ret = KSFT_SKIP;
+ goto out_zram;
+ }
+
+ if (tiers_write("-top") == -EBUSY) { /* blocked while active */
+ swapoff(dev);
+ if (!tiers_write("-top")) /* removable after swapoff */
+ ret = KSFT_PASS;
+ } else {
+ swapoff(dev);
+ }
+out_zram:
+ zram_remove(zidx);
+out_tier:
+ tiers_write("-top");
+ return ret;
+}
+
+static void tiers_clear(void)
+{
+ char buf[4096], *line, *save;
+ int fd;
+ ssize_t n;
+
+ fd = open(TIERS_PATH, O_RDONLY);
+ if (fd < 0)
+ return;
+ n = read(fd, buf, sizeof(buf) - 1);
+ close(fd);
+ if (n < 0)
+ return;
+ buf[n] = '\0';
+
+ for (line = strtok_r(buf, "\n", &save); line;
+ line = strtok_r(NULL, "\n", &save)) {
+ char name[64], cmd[80];
+ int idx, s, e;
+
+ if (sscanf(line, "%63s %d %d %d", name, &idx, &s, &e) != 4)
+ continue;
+ snprintf(cmd, sizeof(cmd), "-%s", name);
+ tiers_write(cmd);
+ }
+}
+
+int main(void)
+{
+ ksft_print_header();
+ ksft_set_plan(7);
+
+ if (geteuid() != 0)
+ ksft_exit_skip("test requires root\n");
+ if (access(TIERS_PATH, F_OK))
+ ksft_exit_skip("%s not present (CONFIG_SWAP/tiers)\n", TIERS_PATH);
+ if (tier_count() != 0)
+ ksft_exit_skip("swap tiers already configured; run on a clean system\n");
+
+ ksft_test_result(test_coverage() == KSFT_PASS, "coverage rule\n");
+ ksft_test_result(test_add() == KSFT_PASS, "add tiers\n");
+ ksft_test_result(test_split() == KSFT_PASS, "split tier\n");
+ ksft_test_result(test_remove() == KSFT_PASS, "remove and merge\n");
+ ksft_test_result(test_errors() == KSFT_PASS, "invalid operations\n");
+ ksft_test_result(test_atomic() == KSFT_PASS, "batch atomicity\n");
+
+ ksft_test_result_code(test_device_pins_tier(), "device pins its tier", NULL);
+
+ tiers_clear();
+
+ ksft_finished();
+}
--
2.34.1
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH v10 6/6] selftests/cgroup: add a swap tier routing test
2026-07-13 2:56 [PATCH v10 0/6] mm/swap, memcg: Introduce swap tiers for cgroup based swap control Youngjun Park
` (4 preceding siblings ...)
2026-07-13 2:56 ` [PATCH v10 5/6] selftests/mm: add a swap tier configuration test Youngjun Park
@ 2026-07-13 2:56 ` Youngjun Park
2026-07-13 15:50 ` [PATCH v10 0/6] mm/swap, memcg: Introduce swap tiers for cgroup based swap control Yosry Ahmed
6 siblings, 0 replies; 20+ messages in thread
From: Youngjun Park @ 2026-07-13 2:56 UTC (permalink / raw)
To: akpm
Cc: chrisl, youngjun.park, linux-mm, cgroups, linux-kernel, kasong,
hannes, mhocko, roman.gushchin, shakeel.butt, muchun.song,
shikemeng, baoquan.he, baohua, yosry, joshua.hahnjy, gunho.lee,
taejoon.song, hyungjun.cho, baver.bae, her0gyugyu
This commit adds a test program for the per-cgroup swap tier control
memory.swap.tiers.max. It checks the default mask, toggling a tier,
rejection of invalid input, and that recreating a tier resets the mask.
It also checks that a cgroup's pages swap only to an allowed tier,
including across the parent and child hierarchy. The routing check uses
two zram devices placed in different tiers.
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Youngjun Park <youngjun.park@lge.com>
---
tools/testing/selftests/cgroup/.gitignore | 1 +
tools/testing/selftests/cgroup/Makefile | 2 +
tools/testing/selftests/cgroup/config | 2 +
.../selftests/cgroup/test_swap_tiers.c | 509 ++++++++++++++++++
4 files changed, 514 insertions(+)
create mode 100644 tools/testing/selftests/cgroup/test_swap_tiers.c
diff --git a/tools/testing/selftests/cgroup/.gitignore b/tools/testing/selftests/cgroup/.gitignore
index 952e4448bf07..77b8e6c3e592 100644
--- a/tools/testing/selftests/cgroup/.gitignore
+++ b/tools/testing/selftests/cgroup/.gitignore
@@ -8,5 +8,6 @@ test_kill
test_kmem
test_memcontrol
test_pids
+test_swap_tiers
test_zswap
wait_inotify
diff --git a/tools/testing/selftests/cgroup/Makefile b/tools/testing/selftests/cgroup/Makefile
index e01584c2189a..a98e3c414cd5 100644
--- a/tools/testing/selftests/cgroup/Makefile
+++ b/tools/testing/selftests/cgroup/Makefile
@@ -16,6 +16,7 @@ TEST_GEN_PROGS += test_kill
TEST_GEN_PROGS += test_kmem
TEST_GEN_PROGS += test_memcontrol
TEST_GEN_PROGS += test_pids
+TEST_GEN_PROGS += test_swap_tiers
TEST_GEN_PROGS += test_zswap
LOCAL_HDRS += $(selfdir)/clone3/clone3_selftests.h $(selfdir)/pidfd/pidfd.h
@@ -32,4 +33,5 @@ $(OUTPUT)/test_kill: $(LIBCGROUP_O)
$(OUTPUT)/test_kmem: $(LIBCGROUP_O)
$(OUTPUT)/test_memcontrol: $(LIBCGROUP_O)
$(OUTPUT)/test_pids: $(LIBCGROUP_O)
+$(OUTPUT)/test_swap_tiers: $(LIBCGROUP_O)
$(OUTPUT)/test_zswap: $(LIBCGROUP_O)
diff --git a/tools/testing/selftests/cgroup/config b/tools/testing/selftests/cgroup/config
index 39f979690dd3..6086bb5bba97 100644
--- a/tools/testing/selftests/cgroup/config
+++ b/tools/testing/selftests/cgroup/config
@@ -4,3 +4,5 @@ CONFIG_CGROUP_FREEZER=y
CONFIG_CGROUP_SCHED=y
CONFIG_MEMCG=y
CONFIG_PAGE_COUNTER=y
+CONFIG_SWAP=y
+CONFIG_ZRAM=y
diff --git a/tools/testing/selftests/cgroup/test_swap_tiers.c b/tools/testing/selftests/cgroup/test_swap_tiers.c
new file mode 100644
index 000000000000..9b4484409ed4
--- /dev/null
+++ b/tools/testing/selftests/cgroup/test_swap_tiers.c
@@ -0,0 +1,509 @@
+// SPDX-License-Identifier: GPL-2.0
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/limits.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <sys/swap.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "kselftest.h"
+#include "cgroup_util.h"
+
+#ifndef MADV_PAGEOUT
+#define MADV_PAGEOUT 21
+#endif
+
+#define TIERS_PATH "/sys/kernel/mm/swap/tiers"
+#define TIERS_MAX "memory.swap.tiers.max"
+
+static int tiers_write(const char *cmd)
+{
+ int fd, ret = 0;
+
+ fd = open(TIERS_PATH, O_WRONLY);
+ if (fd < 0)
+ return -errno;
+ if (write(fd, cmd, strlen(cmd)) < 0)
+ ret = -errno;
+ close(fd);
+ return ret;
+}
+
+static int tier_count(void)
+{
+ char buf[4096], *line, *save;
+ int fd, count = 0;
+ ssize_t n;
+
+ fd = open(TIERS_PATH, O_RDONLY);
+ if (fd < 0)
+ return -1;
+ n = read(fd, buf, sizeof(buf) - 1);
+ close(fd);
+ if (n < 0)
+ return -1;
+ buf[n] = '\0';
+
+ for (line = strtok_r(buf, "\n", &save); line;
+ line = strtok_r(NULL, "\n", &save)) {
+ char name[64];
+ int idx, s, e;
+
+ if (sscanf(line, "%63s %d %d %d", name, &idx, &s, &e) == 4)
+ count++;
+ }
+ return count;
+}
+
+static long swap_used_kb(const char *dev)
+{
+ char line[256];
+ long used = -1;
+ FILE *f;
+
+ f = fopen("/proc/swaps", "r");
+ if (!f)
+ return -1;
+ while (fgets(line, sizeof(line), f)) {
+ char name[128], type[64];
+ long size, u, prio;
+
+ if (sscanf(line, "%127s %63s %ld %ld %ld",
+ name, type, &size, &u, &prio) >= 4 &&
+ !strcmp(name, dev)) {
+ used = u;
+ break;
+ }
+ }
+ fclose(f);
+ return used;
+}
+
+static int swap_active_count(void)
+{
+ char line[256];
+ int n = 0;
+ FILE *f;
+
+ f = fopen("/proc/swaps", "r");
+ if (!f)
+ return -1;
+ if (fgets(line, sizeof(line), f))
+ while (fgets(line, sizeof(line), f))
+ n++;
+ fclose(f);
+ return n;
+}
+
+static void zram_remove(int idx);
+static int zram_add(long size)
+{
+ char path[128], val[64];
+ ssize_t n;
+ int idx, fd;
+
+ fd = open("/sys/class/zram-control/hot_add", O_RDONLY);
+ if (fd < 0)
+ return -1;
+ n = read(fd, val, sizeof(val) - 1);
+ close(fd);
+ if (n <= 0)
+ return -1;
+ val[n] = '\0';
+ idx = atoi(val);
+
+ snprintf(path, sizeof(path), "/sys/block/zram%d/disksize", idx);
+ fd = open(path, O_WRONLY);
+ if (fd < 0) {
+ zram_remove(idx);
+ return -1;
+ }
+
+ snprintf(val, sizeof(val), "%ld", size);
+ n = write(fd, val, strlen(val));
+ close(fd);
+
+ if (n != strlen(val)) {
+ zram_remove(idx);
+ return -1;
+ }
+
+ return idx;
+}
+
+static void zram_remove(int idx)
+{
+ char val[16];
+ int fd;
+
+ fd = open("/sys/class/zram-control/hot_remove", O_WRONLY);
+ if (fd < 0)
+ return;
+ snprintf(val, sizeof(val), "%d", idx);
+ write(fd, val, strlen(val)); /* ignore: best-effort cleanup */
+ close(fd);
+}
+
+static int swap_setup(const char *dev, int prio)
+{
+ char cmd[128];
+
+ snprintf(cmd, sizeof(cmd), "mkswap %s >/dev/null 2>&1", dev);
+ if (system(cmd))
+ return -1;
+ return swapon(dev, SWAP_FLAG_PREFER | (prio & SWAP_FLAG_PRIO_MASK));
+}
+
+static int test_default(const char *root)
+{
+ char *cg = cg_name(root, "swaptier_default");
+ int ret = KSFT_FAIL;
+
+ if (!cg || cg_create(cg))
+ goto out;
+ if (!cg_read_strstr(cg, TIERS_MAX, "fast max") &&
+ !cg_read_strstr(cg, TIERS_MAX, "slow max"))
+ ret = KSFT_PASS;
+out:
+ if (cg) {
+ cg_destroy(cg);
+ free(cg);
+ }
+ return ret;
+}
+
+static int test_toggle(const char *root)
+{
+ char *cg = cg_name(root, "swaptier_toggle");
+ int ret = KSFT_FAIL;
+
+ if (!cg || cg_create(cg))
+ goto out;
+ if (cg_write(cg, TIERS_MAX, "fast 0"))
+ goto out;
+ if (cg_read_strstr(cg, TIERS_MAX, "fast 0"))
+ goto out;
+ if (cg_write(cg, TIERS_MAX, "fast max"))
+ goto out;
+ if (cg_read_strstr(cg, TIERS_MAX, "fast max"))
+ goto out;
+ ret = KSFT_PASS;
+out:
+ if (cg) {
+ cg_destroy(cg);
+ free(cg);
+ }
+ return ret;
+}
+
+static int test_invalid(const char *root)
+{
+ char *cg = cg_name(root, "swaptier_invalid");
+ int ret = KSFT_FAIL;
+
+ if (!cg || cg_create(cg))
+ goto out;
+ if (!cg_write(cg, TIERS_MAX, "nosuchtier 0"))
+ goto out;
+ if (!cg_write(cg, TIERS_MAX, "fast bogus"))
+ goto out;
+ ret = KSFT_PASS;
+out:
+ if (cg) {
+ cg_destroy(cg);
+ free(cg);
+ }
+ return ret;
+}
+
+static int test_recreate(const char *root)
+{
+ char *cg = cg_name(root, "swaptier_recreate");
+ int ret = KSFT_FAIL;
+
+ if (!cg || cg_create(cg))
+ goto out;
+ if (cg_write(cg, TIERS_MAX, "fast 0"))
+ goto out;
+ if (cg_read_strstr(cg, TIERS_MAX, "fast 0"))
+ goto out;
+ if (tiers_write("-fast") || tiers_write("+fast:10"))
+ goto out;
+ if (cg_read_strstr(cg, TIERS_MAX, "fast max"))
+ goto out;
+ ret = KSFT_PASS;
+out:
+ if (cg) {
+ cg_destroy(cg);
+ free(cg);
+ }
+ return ret;
+}
+
+static int swapout_child(const char *cgroup, void *arg)
+{
+ size_t size = (size_t)arg;
+ char *mem;
+ size_t i;
+ int page_size;
+
+ mem = mmap(NULL, size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ if (mem == MAP_FAILED)
+ return -1;
+
+ page_size = sysconf(_SC_PAGE_SIZE);
+ for (i = 0; i < size; i += page_size)
+ mem[i] = 'x';
+ if (madvise(mem, size, MADV_PAGEOUT))
+ return -1;
+ /* Hold the swap entries while the parent inspects /proc/swaps. */
+ pause();
+ return 0;
+}
+
+static int wait_for_dev(const char *dev, const char *dev2)
+{
+ int i;
+
+ for (i = 0; i < 50; i++) {
+ if (access(dev, F_OK) == 0 && access(dev2, F_OK) == 0)
+ return 0;
+
+ usleep(100000);
+ }
+
+ return -1;
+}
+
+static int run_routing_case(const char *cg)
+{
+ char fast_dev[32], slow_dev[32];
+ int zfast = -1, zslow = -1;
+ long used_fast, used_slow;
+ int ret = KSFT_SKIP;
+ pid_t pid = -1;
+ int i;
+
+ /* Only our devices must be present, so usage is unambiguous. */
+ if (swap_active_count() != 0)
+ return KSFT_SKIP;
+
+ zfast = zram_add(MB(128));
+ if (zfast < 0)
+ goto out;
+ snprintf(fast_dev, sizeof(fast_dev), "/dev/zram%d", zfast);
+
+ zslow = zram_add(MB(128));
+ if (zslow < 0)
+ goto out;
+ snprintf(slow_dev, sizeof(slow_dev), "/dev/zram%d", zslow);
+
+ if (wait_for_dev(fast_dev, slow_dev))
+ goto out;
+
+ /* prio 10 -> 'fast' tier [10, MAX]; prio 0 -> 'slow' tier [-1, 9]. */
+ if (swap_setup(fast_dev, 10) || swap_setup(slow_dev, 0))
+ goto out;
+
+ ret = KSFT_FAIL;
+
+ pid = cg_run_nowait(cg, swapout_child, (void *)MB(64));
+ if (pid < 0)
+ goto out;
+
+ for (i = 0; i < 300; i++) { /* up to ~30s for pageout */
+ if (swap_used_kb(slow_dev) > 0)
+ break;
+ usleep(100000);
+ }
+
+ used_fast = swap_used_kb(fast_dev);
+ used_slow = swap_used_kb(slow_dev);
+ if (used_slow > 0 && used_fast == 0)
+ ret = KSFT_PASS;
+ else
+ ksft_print_msg("routing[%s]: fast=%ldKB slow=%ldKB (want fast=0, slow>0)\n",
+ cg, used_fast, used_slow);
+out:
+ if (pid > 0) {
+ kill(pid, SIGKILL);
+ waitpid(pid, NULL, 0);
+ }
+ if (zfast >= 0) {
+ swapoff(fast_dev);
+ zram_remove(zfast);
+ }
+ if (zslow >= 0) {
+ swapoff(slow_dev);
+ zram_remove(zslow);
+ }
+ return ret;
+}
+
+static int test_routing(const char *root)
+{
+ char *cg = cg_name(root, "swaptier_routing");
+ int ret = KSFT_FAIL;
+
+ if (!cg || cg_create(cg))
+ goto out;
+ if (cg_write(cg, TIERS_MAX, "fast 0"))
+ goto out;
+ ret = run_routing_case(cg);
+out:
+ if (cg) {
+ cg_destroy(cg);
+ free(cg);
+ }
+ return ret;
+}
+
+static char *make_parent(const char *root, const char *name)
+{
+ char *cg = cg_name(root, name);
+
+ if (cg && !cg_create(cg) &&
+ !cg_write(cg, "cgroup.subtree_control", "+memory"))
+ return cg;
+
+ if (cg) {
+ cg_destroy(cg);
+ free(cg);
+ }
+ return NULL;
+}
+
+static int test_routing_parent_wins(const char *root)
+{
+ char *parent = make_parent(root, "swaptier_pwins");
+ char *child = NULL;
+ int ret = KSFT_FAIL;
+
+ if (!parent)
+ goto out;
+ if (cg_write(parent, TIERS_MAX, "fast 0"))
+ goto out;
+
+ child = cg_name(parent, "child");
+ if (!child || cg_create(child))
+ goto out;
+ if (cg_write(child, TIERS_MAX, "fast max")) /* child tries to re-enable */
+ goto out;
+
+ ret = run_routing_case(child);
+out:
+ if (child) {
+ cg_destroy(child);
+ free(child);
+ }
+ if (parent) {
+ cg_destroy(parent);
+ free(parent);
+ }
+ return ret;
+}
+
+static int test_routing_child_restricts(const char *root)
+{
+ char *parent = make_parent(root, "swaptier_crestr");
+ char *child = NULL;
+ int ret = KSFT_FAIL;
+
+ if (!parent)
+ goto out;
+
+ child = cg_name(parent, "child");
+ if (!child || cg_create(child))
+ goto out;
+ if (cg_write(child, TIERS_MAX, "fast 0"))
+ goto out;
+
+ ret = run_routing_case(child);
+out:
+ if (child) {
+ cg_destroy(child);
+ free(child);
+ }
+ if (parent) {
+ cg_destroy(parent);
+ free(parent);
+ }
+ return ret;
+}
+
+static void tiers_clear(void)
+{
+ char buf[4096], *line, *save;
+ int fd;
+ ssize_t n;
+
+ fd = open(TIERS_PATH, O_RDONLY);
+ if (fd < 0)
+ return;
+ n = read(fd, buf, sizeof(buf) - 1);
+ close(fd);
+ if (n < 0)
+ return;
+ buf[n] = '\0';
+
+ for (line = strtok_r(buf, "\n", &save); line;
+ line = strtok_r(NULL, "\n", &save)) {
+ char name[64], cmd[80];
+ int idx, s, e;
+
+ if (sscanf(line, "%63s %d %d %d", name, &idx, &s, &e) != 4)
+ continue;
+ snprintf(cmd, sizeof(cmd), "-%s", name);
+ tiers_write(cmd);
+ }
+}
+
+int main(void)
+{
+ char root[PATH_MAX];
+
+ ksft_print_header();
+ ksft_set_plan(7);
+
+ if (geteuid() != 0)
+ ksft_exit_skip("test requires root\n");
+ if (cg_find_unified_root(root, sizeof(root), NULL))
+ ksft_exit_skip("cgroup v2 isn't mounted\n");
+ if (cg_read_strstr(root, "cgroup.controllers", "memory"))
+ ksft_exit_skip("memory controller isn't available\n");
+ if (cg_read_strstr(root, "cgroup.subtree_control", "memory"))
+ if (cg_write(root, "cgroup.subtree_control", "+memory"))
+ ksft_exit_skip("failed to enable memory controller\n");
+ if (access(TIERS_PATH, F_OK))
+ ksft_exit_skip("swap tiers interface not present\n");
+ if (tier_count() != 0)
+ ksft_exit_skip("swap tiers already configured; run on a clean system\n");
+
+ /* Two tiers: fast = [10, MAX], slow = [-1, 9]. */
+ if (tiers_write("+slow:-1 +fast:10"))
+ ksft_exit_skip("failed to configure swap tiers\n");
+
+ ksft_test_result(test_default(root) == KSFT_PASS, "default mask is max\n");
+ ksft_test_result(test_toggle(root) == KSFT_PASS, "enable/disable tier\n");
+ ksft_test_result(test_invalid(root) == KSFT_PASS, "invalid input rejected\n");
+ ksft_test_result(test_recreate(root) == KSFT_PASS,
+ "recreated tier resets cgroup mask\n");
+
+ ksft_test_result_code(test_routing(root),
+ "swapout honors tier mask", NULL);
+ ksft_test_result_code(test_routing_parent_wins(root),
+ "child cannot re-enable a parent-disabled tier", NULL);
+ ksft_test_result_code(test_routing_child_restricts(root),
+ "child can restrict tiers below its parent", NULL);
+
+ tiers_clear();
+
+ ksft_finished();
+}
--
2.34.1
^ permalink raw reply related [flat|nested] 20+ messages in thread
* Re: [PATCH v10 2/6] mm: swap: associate swap devices with tiers
2026-07-13 2:56 ` [PATCH v10 2/6] mm: swap: associate swap devices with tiers Youngjun Park
@ 2026-07-13 14:28 ` Usama Arif
0 siblings, 0 replies; 20+ messages in thread
From: Usama Arif @ 2026-07-13 14:28 UTC (permalink / raw)
To: Youngjun Park
Cc: Usama Arif, akpm, chrisl, linux-mm, cgroups, linux-kernel, kasong,
hannes, mhocko, roman.gushchin, shakeel.butt, muchun.song,
shikemeng, baoquan.he, baohua, yosry, joshua.hahnjy, gunho.lee,
taejoon.song, hyungjun.cho, baver.bae, her0gyugyu
On Mon, 13 Jul 2026 11:56:40 +0900 Youngjun Park <youngjun.park@lge.com> wrote:
> This patch connects swap devices to the swap tier infrastructure,
> ensuring that devices are correctly assigned to tiers based on their
> priority.
>
> A `tier_mask` is added to identify the tier membership of swap devices.
> Although tier-based allocation logic is not yet implemented, this
> mapping is necessary to track which tier a device belongs to. Upon
> activation, the device is assigned to a tier by matching its priority
> against the configured tier ranges.
>
> The infrastructure allows dynamic modification of tiers, such as
> splitting or merging ranges. These operations are permitted provided
> that the tier assignment of already configured swap devices remains
> unchanged.
>
> This patch also adds the documentation for the swap tier feature,
> covering the core concepts, sysfs interface usage, and configuration
> details.
>
> Reviewed-by: Baoquan He <baoquan.he@linux.dev>
> Signed-off-by: Youngjun Park <youngjun.park@lge.com>
> ---
> Documentation/mm/index.rst | 1 +
> Documentation/mm/swap-tier.rst | 150 +++++++++++++++++++++++++++++++++
> MAINTAINERS | 1 +
> include/linux/swap.h | 1 +
> mm/swap_state.c | 2 +-
> mm/swap_tier.c | 101 +++++++++++++++++++---
> mm/swap_tier.h | 13 ++-
> mm/swapfile.c | 2 +
> 8 files changed, 257 insertions(+), 14 deletions(-)
> create mode 100644 Documentation/mm/swap-tier.rst
>
> diff --git a/Documentation/mm/index.rst b/Documentation/mm/index.rst
> index 13a79f5d092c..6afc45cd4b3d 100644
> --- a/Documentation/mm/index.rst
> +++ b/Documentation/mm/index.rst
> @@ -34,6 +34,7 @@ see the :doc:`admin guide <../admin-guide/mm/index>`.
> page_reclaim
> swap
> swap-table
> + swap-tier
> page_cache
> shmfs
> oom
> diff --git a/Documentation/mm/swap-tier.rst b/Documentation/mm/swap-tier.rst
> new file mode 100644
> index 000000000000..0fb4a1153a67
> --- /dev/null
> +++ b/Documentation/mm/swap-tier.rst
> @@ -0,0 +1,150 @@
> +.. SPDX-License-Identifier: GPL-2.0
> +
> +:Author: Chris Li <chrisl@kernel.org> Youngjun Park <youngjun.park@lge.com>
> +
> +==========
> +Swap Tier
> +==========
> +
> +Swap tier is a collection of user-named groups classified by priority ranges.
> +It acts as a facilitation layer, allowing users to manage swap devices based
> +on their speeds.
> +
> +Users are encouraged to assign swap device priorities according to device
> +speed to fully utilize this feature. While the current implementation is
> +integrated with cgroups, the concept is designed to be extensible for other
> +subsystems in the future.
> +
> +Priority Range
> +--------------
> +
> +The specified tiers must cover the entire priority range from -1
> +(DEF_SWAP_PRIO) to SHRT_MAX.
> +
> +Consistency
> +-----------
> +
> +Tier consistency is guaranteed with a focus on maximizing flexibility. When a
> +swap device is activated within a tier range, the tier covering that device's
> +priority is guaranteed not to disappear or change while the device remains
> +active. Adding a new tier may split the range of an existing tier, but the
> +active device's tier assignment remains unchanged.
> +
> +However, specifying a tier in a cgroup does not guarantee the tier's existence.
> +Consequently, the corresponding tier can disappear at any time.
> +
> +Configuration Interface
> +-----------------------
> +
> +The swap tiers can be configured via the following interface:
> +
> +/sys/kernel/mm/swap/tiers
> +
> +Operations can be performed using the following syntax:
> +
> +* Add: ``+"<tiername>":"<start_priority>"``
> +* Remove: ``-"<tiername>"``
> +
> +Tier names must consist of alphanumeric characters and underscores. Multiple
> +operations can be provided in a single write, separated by commas (",") or
> +whitespace (spaces, tabs, newlines).
> +
> +When configuring tiers, the specified value represents the **start priority**
> +of that tier. The end priority is automatically determined by the start
> +priority of the next higher tier. Consequently, adding a tier
> +automatically adjusts the ranges of adjacent tiers to ensure continuity.
> +
> +Examples
> +--------
> +
> +**1. Initialization**
> +
> +A tier starting at -1 is mandatory to cover the entire priority range up to
> +SHRT_MAX. In this example, 'HDD' starts at 50, and 'NET' covers the remaining
> +lower range starting from -1.
> +
> +::
> +
> + # echo "+HDD:50, +NET:-1" > /sys/kernel/mm/swap/tiers
> + # cat /sys/kernel/mm/swap/tiers
> + Name Idx PrioStart PrioEnd
> + HDD 0 50 32767
> + NET 1 -1 49
> +
> +**2. Adding a New Tier (split)**
> +
> +A new tier 'SSD' is added at priority 100, splitting the existing 'HDD' tier.
> +The ranges are automatically recalculated:
> +
> +* 'SSD' takes the top range (100 to SHRT_MAX).
> +* 'HDD' is adjusted to the range between 'NET' and 'SSD' (50 to 99).
> +* 'NET' remains unchanged (-1 to 49).
> +
> +::
> +
> + # echo "+SSD:100" > /sys/kernel/mm/swap/tiers
> + # cat /sys/kernel/mm/swap/tiers
> + Name Idx PrioStart PrioEnd
> + SSD 2 100 32767
> + HDD 0 50 99
> + NET 1 -1 49
> +
> +**3. Removal (merge)**
> +
> +Tiers can be removed using the '-' prefix.
> +::
> +
> + # echo "-SSD" > /sys/kernel/mm/swap/tiers
> +
> +When a tier is removed, its priority range is merged into the adjacent
> +tier. The merge direction is always upward (the tier below expands),
> +except when the lowest tier is removed — in that case the tier above
> +shifts its starting priority down to -1 to maintain full range coverage.
> +
> +::
> +
> + Initial state:
> + Name Idx PrioStart PrioEnd
> + SSD 2 100 32767
> + HDD 1 50 99
> + NET 0 -1 49
> +
> + # echo "-SSD" > /sys/kernel/mm/swap/tiers
> +
> + Name Idx PrioStart PrioEnd
> + HDD 1 50 32767 <- merged with SSD's range
> + NET 0 -1 49
> +
> + # echo "-NET" > /sys/kernel/mm/swap/tiers
> +
> + Name Idx PrioStart PrioEnd
> + HDD 1 -1 32767 <- shifted down to -1
> +
> +**4. Interaction with Active Swap Devices**
> +
> +If a swap device is active (swapon), the tier covering that device's
> +priority cannot be removed. Splitting the active tier's range is only
> +allowed above the device's priority.
> +
> +Assume a swap device is active at priority 60 (inside 'HDD' tier).
> +
> +::
> +
> + # swapon -p 60 /dev/zram0
> +
> + Name Idx PrioStart PrioEnd
> + HDD 0 50 32767
> + NET 1 -1 49
> +
> + # echo "-HDD" > /sys/kernel/mm/swap/tiers
> + -bash: echo: write error: Device or resource busy
> +
> + # echo "+SSD:60" > /sys/kernel/mm/swap/tiers
> + -bash: echo: write error: Device or resource busy
> +
> + # echo "+SSD:100" > /sys/kernel/mm/swap/tiers
> +
> + Name Idx PrioStart PrioEnd
> + SSD 2 100 32767
> + HDD 0 50 99 <- device (prio 60) stays here
> + NET 1 -1 49
> diff --git a/MAINTAINERS b/MAINTAINERS
> index e94d1af17c39..4485a0650984 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -17237,6 +17237,7 @@ L: linux-mm@kvack.org
> S: Maintained
> F: Documentation/ABI/testing/sysfs-kernel-mm-swap
> F: Documentation/mm/swap-table.rst
> +F: Documentation/mm/swap-tier.rst
> F: include/linux/swap.h
> F: include/linux/swapfile.h
> F: include/linux/swapops.h
> diff --git a/include/linux/swap.h b/include/linux/swap.h
> index 4427c7aa16dc..2d647a8fa60f 100644
> --- a/include/linux/swap.h
> +++ b/include/linux/swap.h
> @@ -244,6 +244,7 @@ struct swap_info_struct {
> struct percpu_ref users; /* indicate and keep swap device valid. */
> unsigned long flags; /* SWP_USED etc: see above */
> signed short prio; /* swap priority of this type */
> + int tier_mask; /* swap tier mask */
> struct plist_node list; /* entry in swap_active_head */
> signed char type; /* strange name for an index */
> unsigned int max; /* size of this swap device */
> diff --git a/mm/swap_state.c b/mm/swap_state.c
> index c18ff741f2e0..dcec1a6c92bd 100644
> --- a/mm/swap_state.c
> +++ b/mm/swap_state.c
> @@ -1095,7 +1095,7 @@ static ssize_t tiers_store(struct kobject *kobj,
> }
> }
>
> - if (!swap_tiers_validate()) {
> + if (!swap_tiers_update()) {
> ret = -EINVAL;
> goto restore;
> }
> diff --git a/mm/swap_tier.c b/mm/swap_tier.c
> index ac7a3c2a48cb..6b57cadb3e95 100644
> --- a/mm/swap_tier.c
> +++ b/mm/swap_tier.c
> @@ -38,6 +38,8 @@ static LIST_HEAD(swap_tier_inactive_list);
> (!list_is_first(&(tier)->list, &swap_tier_active_list) ? \
> list_prev_entry((tier), list)->prio - 1 : SHRT_MAX)
>
> +#define MASK_TO_TIER(mask) (&swap_tiers[__ffs((mask))])
> +
> #define for_each_tier(tier, idx) \
> for (idx = 0, tier = &swap_tiers[0]; idx < MAX_SWAPTIER; \
> idx++, tier = &swap_tiers[idx])
> @@ -59,6 +61,26 @@ static bool swap_tier_is_active(void)
> return !list_empty(&swap_tier_active_list);
> }
>
> +static bool swap_tier_prio_in_range(struct swap_tier *tier, short prio)
> +{
> + if (tier->prio <= prio && TIER_END_PRIO(tier) >= prio)
> + return true;
> +
> + return false;
> +}
> +
> +static bool swap_tier_prio_is_used(short prio)
> +{
> + struct swap_tier *tier;
> +
> + for_each_active_tier(tier) {
> + if (tier->prio == prio)
> + return true;
> + }
> +
> + return false;
> +}
> +
> static struct swap_tier *swap_tier_lookup(const char *name)
> {
> struct swap_tier *tier;
> @@ -99,6 +121,7 @@ void swap_tiers_init(void)
> int idx;
>
> BUILD_BUG_ON(BITS_PER_TYPE(int) < MAX_SWAPTIER);
> + BUILD_BUG_ON(MAX_SWAPTIER > TIER_DEFAULT_IDX);
>
> for_each_tier(tier, idx) {
> INIT_LIST_HEAD(&tier->list);
> @@ -149,17 +172,29 @@ static struct swap_tier *swap_tier_prepare(const char *name, short prio)
> return tier;
> }
>
> -static int swap_tier_check_range(short prio)
> +static int swap_tier_can_split_range(short new_prio)
> {
> + struct swap_info_struct *p;
> struct swap_tier *tier;
>
> lockdep_assert_held(&swap_lock);
> lockdep_assert_held(&swap_tier_lock);
>
> - for_each_active_tier(tier) {
> - /* No overwrite */
> - if (tier->prio == prio)
> - return -EINVAL;
> + plist_for_each_entry(p, &swap_active_head, list) {
> + if (p->tier_mask == TIER_DEFAULT_MASK)
> + continue;
> +
> + tier = MASK_TO_TIER(p->tier_mask);
> + if (!swap_tier_prio_in_range(tier, new_prio))
> + continue;
> +
> + /*
> + * Device sits in a tier that spans new_prio;
> + * splitting here would reassign it to a
> + * different tier.
> + */
> + if (p->prio >= new_prio)
> + return -EBUSY;
> }
>
> return 0;
> @@ -199,7 +234,11 @@ int swap_tiers_add(const char *name, int prio)
> if (!swap_tier_validate_name(name))
> return -EINVAL;
>
> - ret = swap_tier_check_range(prio);
> + /* No overwrite */
> + if (swap_tier_prio_is_used(prio))
> + return -EBUSY;
> +
> + ret = swap_tier_can_split_range(prio);
> if (ret)
> return ret;
>
> @@ -226,6 +265,11 @@ int swap_tiers_remove(const char *name)
> if (!tier)
> return -EINVAL;
>
> + /* Simulate adding a tier to check for conflicts */
> + ret = swap_tier_can_split_range(tier->prio);
> + if (ret)
> + return ret;
> +
> /* Removing DEF_SWAP_PRIO merges into the higher tier. */
> if (!list_is_singular(&swap_tier_active_list)
> && tier->prio == DEF_SWAP_PRIO)
> @@ -236,13 +280,15 @@ int swap_tiers_remove(const char *name)
> return ret;
> }
>
> -static struct swap_tier swap_tiers_snap[MAX_SWAPTIER];
> /*
> - * XXX: When multiple operations (adds and removes) are submitted in a
> - * single write, reverting each individually on failure is complex and
> - * error-prone. Instead, snapshot the entire state beforehand and
> - * restore it wholesale if any operation fails.
> + * XXX: Static global snapshot buffer for batch operations. Small
> + * and used once per write, so a static global is not bad.
> + * When multiple adds/removes are submitted in a single write,
> + * reverting each individually on failure is error-prone. Instead,
> + * snapshot beforehand and restore wholesale if any operation fails.
> */
> +static struct swap_tier swap_tiers_snap[MAX_SWAPTIER];
> +
> void swap_tiers_snapshot(void)
> {
> BUILD_BUG_ON(sizeof(swap_tiers_snap) != sizeof(swap_tiers));
> @@ -282,10 +328,30 @@ void swap_tiers_snapshot_restore(void)
> }
> }
>
> -bool swap_tiers_validate(void)
> +void swap_tiers_assign_dev(struct swap_info_struct *swp)
> {
> struct swap_tier *tier;
>
> + lockdep_assert_held(&swap_lock);
> +
> + for_each_active_tier(tier) {
> + if (swap_tier_prio_in_range(tier, swp->prio)) {
> + swp->tier_mask = TIER_MASK(tier);
> + return;
> + }
> + }
> +
> + swp->tier_mask = TIER_DEFAULT_MASK;
> +}
> +
> +bool swap_tiers_update(void)
> +{
> + struct swap_tier *tier;
> + struct swap_info_struct *swp;
> +
> + lockdep_assert_held(&swap_lock);
> + lockdep_assert_held(&swap_tier_lock);
> +
> /*
> * Initial setting might not cover DEF_SWAP_PRIO.
> * Swap tier must cover the full range (DEF_SWAP_PRIO to SHRT_MAX).
> @@ -298,5 +364,16 @@ bool swap_tiers_validate(void)
> return false;
> }
>
> + /*
> + * If applied initially, the swap tier_mask may change
> + * from the default value.
> + */
> + plist_for_each_entry(swp, &swap_active_head, list) {
> + /* Tier is already configured */
> + if (swp->tier_mask != TIER_DEFAULT_MASK)
> + break;
> + swap_tiers_assign_dev(swp);
> + }
> +
> return true;
> }
> diff --git a/mm/swap_tier.h b/mm/swap_tier.h
> index a1395ec02c24..3e355f857363 100644
> --- a/mm/swap_tier.h
> +++ b/mm/swap_tier.h
> @@ -5,8 +5,15 @@
> #include <linux/types.h>
> #include <linux/spinlock.h>
>
> +/* Forward declarations */
> +struct swap_info_struct;
> +
> extern spinlock_t swap_tier_lock;
>
> +#define TIER_ALL_MASK (~0)
> +#define TIER_DEFAULT_IDX (31)
> +#define TIER_DEFAULT_MASK (1U << TIER_DEFAULT_IDX)
> +
> /* Initialization and application */
> void swap_tiers_init(void);
> ssize_t swap_tiers_sysfs_show(char *buf);
> @@ -16,5 +23,9 @@ int swap_tiers_remove(const char *name);
>
> void swap_tiers_snapshot(void);
> void swap_tiers_snapshot_restore(void);
> -bool swap_tiers_validate(void);
> +bool swap_tiers_update(void);
> +
> +/* Tier assignment */
> +void swap_tiers_assign_dev(struct swap_info_struct *swp);
> +
> #endif /* _SWAP_TIER_H */
> diff --git a/mm/swapfile.c b/mm/swapfile.c
> index ff567ad893a4..f3cff586cf30 100644
> --- a/mm/swapfile.c
> +++ b/mm/swapfile.c
> @@ -3041,6 +3041,8 @@ static void _enable_swap_info(struct swap_info_struct *si)
>
> /* Add back to available list */
> add_to_avail_list(si, true);
> +
> + swap_tiers_assign_dev(si);
Could we move the assignment before the device is added to the active and
available lists?
After patch 4, swap allocation checks si->tier_mask while holding only
swap_avail_lock. But here the new device is added to swap_avail_head
before swap_tiers_assign_dev() initializes its mask.
That creates a small window where reclaim can see this swap_info_struct
with a stale tier_mask. swap_info_struct instances are reused across
swapoff/swapon, so the stale mask can come from the previous device that
occupied this slot. A memcg allowed to use the old tier could then
temporarily allocate from the newly enabled device even if that device
belongs to a different tier.
> }
>
> /*
> --
> 2.34.1
>
>
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH v10 0/6] mm/swap, memcg: Introduce swap tiers for cgroup based swap control
2026-07-13 2:56 [PATCH v10 0/6] mm/swap, memcg: Introduce swap tiers for cgroup based swap control Youngjun Park
` (5 preceding siblings ...)
2026-07-13 2:56 ` [PATCH v10 6/6] selftests/cgroup: add a swap tier routing test Youngjun Park
@ 2026-07-13 15:50 ` Yosry Ahmed
2026-07-13 15:57 ` Youngjun Park
2026-07-13 17:05 ` Chris Li
6 siblings, 2 replies; 20+ messages in thread
From: Yosry Ahmed @ 2026-07-13 15:50 UTC (permalink / raw)
To: Youngjun Park
Cc: akpm, chrisl, linux-mm, cgroups, linux-kernel, kasong, hannes,
mhocko, roman.gushchin, shakeel.butt, muchun.song, shikemeng,
baoquan.he, baohua, joshua.hahnjy, gunho.lee, taejoon.song,
hyungjun.cho, baver.bae, her0gyugyu
On Sun, Jul 12, 2026 at 7:57 PM Youngjun Park <youngjun.park@lge.com> wrote:
>
> This is the v10 series of the swap tier patchset.
>
> v10 folds in the Sashiko review fixes for the selftests added in v9 and
> rebases onto the current mm-new. There are no functional changes to the
> core swap or memcg code since v9; see the changelog for details.
>
> For context, the bulk of the series is unchanged since v8, with great thanks
> to Shakeel Butt and Yosry for the reviews and discussions [1] that shaped it.
> The main change in v8 was the interface change to use memory.swap.tiers.max
> with '0' (disable) and 'max' (enable) values. This mechanism was suggested
> by Shakeel and Yosry.
>
> This change allows for future extensions to control swap between tiers and
> aligns better with existing memcg interfaces. It is confined to patch #3's
> user-facing interface; internally, patch #3 still uses the existing mask
> processing method, which is implementation-efficient.
>
> We also discussed tier extensions. Thanks to Yosry, Nhat and Shakeel for their
> valuable feedback.
>
> Here is a brief summary of our tentative conclusions. Please correct me
> if anything is misrepresented (details in references):
>
> * Zswap tiering [2]:
> Zswap can itself be a tier (typically the fastest one). But, until vswap lands,
> zswap cannot be the only allowed tier,
> since it still needs a physical device for allocation;
> that restriction can be lifted once vswap is supported.
Does this series support zswap being a tier? I cannot find any mention
of zswap in the patches.
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH v10 0/6] mm/swap, memcg: Introduce swap tiers for cgroup based swap control
2026-07-13 15:50 ` [PATCH v10 0/6] mm/swap, memcg: Introduce swap tiers for cgroup based swap control Yosry Ahmed
@ 2026-07-13 15:57 ` Youngjun Park
2026-07-13 16:01 ` Yosry Ahmed
2026-07-13 17:05 ` Chris Li
1 sibling, 1 reply; 20+ messages in thread
From: Youngjun Park @ 2026-07-13 15:57 UTC (permalink / raw)
To: Yosry Ahmed
Cc: akpm, chrisl, linux-mm, cgroups, linux-kernel, kasong, hannes,
mhocko, roman.gushchin, shakeel.butt, muchun.song, shikemeng,
baoquan.he, baohua, joshua.hahnjy, gunho.lee, taejoon.song,
hyungjun.cho, baver.bae, her0gyugyu
On Mon, Jul 13, 2026 at 08:50:36AM -0700, Yosry Ahmed wrote:
> On Sun, Jul 12, 2026 at 7:57 PM Youngjun Park <youngjun.park@lge.com> wrote:
> >
> > This is the v10 series of the swap tier patchset.
> >
> > v10 folds in the Sashiko review fixes for the selftests added in v9 and
> > rebases onto the current mm-new. There are no functional changes to the
> > core swap or memcg code since v9; see the changelog for details.
> >
> > For context, the bulk of the series is unchanged since v8, with great thanks
> > to Shakeel Butt and Yosry for the reviews and discussions [1] that shaped it.
> > The main change in v8 was the interface change to use memory.swap.tiers.max
> > with '0' (disable) and 'max' (enable) values. This mechanism was suggested
> > by Shakeel and Yosry.
> >
> > This change allows for future extensions to control swap between tiers and
> > aligns better with existing memcg interfaces. It is confined to patch #3's
> > user-facing interface; internally, patch #3 still uses the existing mask
> > processing method, which is implementation-efficient.
> >
> > We also discussed tier extensions. Thanks to Yosry, Nhat and Shakeel for their
> > valuable feedback.
> >
> > Here is a brief summary of our tentative conclusions. Please correct me
> > if anything is misrepresented (details in references):
> >
> > * Zswap tiering [2]:
> > Zswap can itself be a tier (typically the fastest one). But, until vswap lands,
> > zswap cannot be the only allowed tier,
> > since it still needs a physical device for allocation;
> > that restriction can be lifted once vswap is supported.
>
> Does this series support zswap being a tier? I cannot find any mention
> of zswap in the patches.
Hello Yosry!
This series does not cover zswap as a tier yet.
My plan is to land the swap tier infrastructure together with the
first use case (cgroup-based swap control) first, and then follow
up with zswap tier support in a subsequent series, continuing the
discussions we've had above.
(I mentioned on cover letter, right above the overview section)
Does that approach sound reasonable to you?
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH v10 0/6] mm/swap, memcg: Introduce swap tiers for cgroup based swap control
2026-07-13 15:57 ` Youngjun Park
@ 2026-07-13 16:01 ` Yosry Ahmed
2026-07-13 16:22 ` Youngjun Park
2026-07-13 17:02 ` Chris Li
0 siblings, 2 replies; 20+ messages in thread
From: Yosry Ahmed @ 2026-07-13 16:01 UTC (permalink / raw)
To: Youngjun Park
Cc: akpm, chrisl, linux-mm, cgroups, linux-kernel, kasong, hannes,
mhocko, roman.gushchin, shakeel.butt, muchun.song, shikemeng,
baoquan.he, baohua, joshua.hahnjy, gunho.lee, taejoon.song,
hyungjun.cho, baver.bae, her0gyugyu
On Mon, Jul 13, 2026 at 8:57 AM Youngjun Park <youngjun.park@lge.com> wrote:
>
> On Mon, Jul 13, 2026 at 08:50:36AM -0700, Yosry Ahmed wrote:
> > On Sun, Jul 12, 2026 at 7:57 PM Youngjun Park <youngjun.park@lge.com> wrote:
> > >
> > > This is the v10 series of the swap tier patchset.
> > >
> > > v10 folds in the Sashiko review fixes for the selftests added in v9 and
> > > rebases onto the current mm-new. There are no functional changes to the
> > > core swap or memcg code since v9; see the changelog for details.
> > >
> > > For context, the bulk of the series is unchanged since v8, with great thanks
> > > to Shakeel Butt and Yosry for the reviews and discussions [1] that shaped it.
> > > The main change in v8 was the interface change to use memory.swap.tiers.max
> > > with '0' (disable) and 'max' (enable) values. This mechanism was suggested
> > > by Shakeel and Yosry.
> > >
> > > This change allows for future extensions to control swap between tiers and
> > > aligns better with existing memcg interfaces. It is confined to patch #3's
> > > user-facing interface; internally, patch #3 still uses the existing mask
> > > processing method, which is implementation-efficient.
> > >
> > > We also discussed tier extensions. Thanks to Yosry, Nhat and Shakeel for their
> > > valuable feedback.
> > >
> > > Here is a brief summary of our tentative conclusions. Please correct me
> > > if anything is misrepresented (details in references):
> > >
> > > * Zswap tiering [2]:
> > > Zswap can itself be a tier (typically the fastest one). But, until vswap lands,
> > > zswap cannot be the only allowed tier,
> > > since it still needs a physical device for allocation;
> > > that restriction can be lifted once vswap is supported.
> >
> > Does this series support zswap being a tier? I cannot find any mention
> > of zswap in the patches.
>
> Hello Yosry!
>
> This series does not cover zswap as a tier yet.
>
> My plan is to land the swap tier infrastructure together with the
> first use case (cgroup-based swap control) first, and then follow
> up with zswap tier support in a subsequent series, continuing the
> discussions we've had above.
> (I mentioned on cover letter, right above the overview section)
>
> Does that approach sound reasonable to you?
How does swap tiering work with zswap in the current series? I assume
zswap is just enabled for all devices in all tiers? I wonder if
introducing zswap as a tier after the fact changes user-visible
behavior. I guess if zswap will be introduced with a default "max"
value it will more-or-less be the same behavior, but I would check all
user-visible behaviors related to zswap (e.g. interaction with other
zswap interfaces) to make sure nothing breaks or changes in a
meaningful way when zswap is introduced as a tier later.
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH v10 0/6] mm/swap, memcg: Introduce swap tiers for cgroup based swap control
2026-07-13 16:01 ` Yosry Ahmed
@ 2026-07-13 16:22 ` Youngjun Park
2026-07-13 17:02 ` Chris Li
1 sibling, 0 replies; 20+ messages in thread
From: Youngjun Park @ 2026-07-13 16:22 UTC (permalink / raw)
To: Yosry Ahmed
Cc: akpm, chrisl, linux-mm, cgroups, linux-kernel, kasong, hannes,
mhocko, roman.gushchin, shakeel.butt, muchun.song, shikemeng,
baoquan.he, baohua, joshua.hahnjy, gunho.lee, taejoon.song,
hyungjun.cho, baver.bae, her0gyugyu
On Mon, Jul 13, 2026 at 09:01:20AM -0700, Yosry Ahmed wrote:
> On Mon, Jul 13, 2026 at 8:57 AM Youngjun Park <youngjun.park@lge.com> wrote:
> >
> > On Mon, Jul 13, 2026 at 08:50:36AM -0700, Yosry Ahmed wrote:
> > > On Sun, Jul 12, 2026 at 7:57 PM Youngjun Park <youngjun.park@lge.com> wrote:
> > > >
> > > > This is the v10 series of the swap tier patchset.
> > > >
> > > > v10 folds in the Sashiko review fixes for the selftests added in v9 and
> > > > rebases onto the current mm-new. There are no functional changes to the
> > > > core swap or memcg code since v9; see the changelog for details.
> > > >
> > > > For context, the bulk of the series is unchanged since v8, with great thanks
> > > > to Shakeel Butt and Yosry for the reviews and discussions [1] that shaped it.
> > > > The main change in v8 was the interface change to use memory.swap.tiers.max
> > > > with '0' (disable) and 'max' (enable) values. This mechanism was suggested
> > > > by Shakeel and Yosry.
> > > >
> > > > This change allows for future extensions to control swap between tiers and
> > > > aligns better with existing memcg interfaces. It is confined to patch #3's
> > > > user-facing interface; internally, patch #3 still uses the existing mask
> > > > processing method, which is implementation-efficient.
> > > >
> > > > We also discussed tier extensions. Thanks to Yosry, Nhat and Shakeel for their
> > > > valuable feedback.
> > > >
> > > > Here is a brief summary of our tentative conclusions. Please correct me
> > > > if anything is misrepresented (details in references):
> > > >
> > > > * Zswap tiering [2]:
> > > > Zswap can itself be a tier (typically the fastest one). But, until vswap lands,
> > > > zswap cannot be the only allowed tier,
> > > > since it still needs a physical device for allocation;
> > > > that restriction can be lifted once vswap is supported.
> > >
> > > Does this series support zswap being a tier? I cannot find any mention
> > > of zswap in the patches.
> >
> > Hello Yosry!
> >
> > This series does not cover zswap as a tier yet.
> >
> > My plan is to land the swap tier infrastructure together with the
> > first use case (cgroup-based swap control) first, and then follow
> > up with zswap tier support in a subsequent series, continuing the
> > discussions we've had above.
> > (I mentioned on cover letter, right above the overview section)
> >
> > Does that approach sound reasonable to you?
>
> How does swap tiering work with zswap in the current series? I assume
> zswap is just enabled for all devices in all tiers?
Yes, that's correct.
> I wonder if introducing zswap as a tier after the fact changes user-visible
> behavior. I guess if zswap will be introduced with a default "max"
> value it will more-or-less be the same behavior,
Right, that's the plan.
> but I would check all
> user-visible behaviors related to zswap (e.g. interaction with other
> zswap interfaces) to make sure nothing breaks or changes in a
> meaningful way when zswap is introduced as a tier later.
Fair point. Let me review this more and get back to you!
Thanks,
Youngjun
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH v10 0/6] mm/swap, memcg: Introduce swap tiers for cgroup based swap control
2026-07-13 16:01 ` Yosry Ahmed
2026-07-13 16:22 ` Youngjun Park
@ 2026-07-13 17:02 ` Chris Li
2026-07-13 17:11 ` Yosry Ahmed
1 sibling, 1 reply; 20+ messages in thread
From: Chris Li @ 2026-07-13 17:02 UTC (permalink / raw)
To: Yosry Ahmed
Cc: Youngjun Park, akpm, linux-mm, cgroups, linux-kernel, kasong,
hannes, mhocko, roman.gushchin, shakeel.butt, muchun.song,
shikemeng, baoquan.he, baohua, joshua.hahnjy, gunho.lee,
taejoon.song, hyungjun.cho, baver.bae, her0gyugyu
On Mon, Jul 13, 2026 at 9:01 AM Yosry Ahmed <yosry@kernel.org> wrote:
> > My plan is to land the swap tier infrastructure together with the
> > first use case (cgroup-based swap control) first, and then follow
> > up with zswap tier support in a subsequent series, continuing the
> > discussions we've had above.
> > (I mentioned on cover letter, right above the overview section)
> >
> > Does that approach sound reasonable to you?
>
> How does swap tiering work with zswap in the current series? I assume
> zswap is just enabled for all devices in all tiers? I wonder if
Zswap is not part of the tiers exactly because it sits in front of all
swap devices (tiers) and uses a different control to enable or disable
it.
Let's not combine these two; let zswap use its own existing cgroup
control interface.
> introducing zswap as a tier after the fact changes user-visible
> behavior. I guess if zswap will be introduced with a default "max"
> value it will more-or-less be the same behavior, but I would check all
> user-visible behaviors related to zswap (e.g. interaction with other
> zswap interfaces) to make sure nothing breaks or changes in a
> meaningful way when zswap is introduced as a tier later.
Zswap will not be introduced as a tier. The existing user interface
makes zswap not exactly compatible with the tier ordering because it
sits in front of every swapfile. If we change that, we break the user
interface. I suggest we keep zswap working as it is now.
Chris
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH v10 0/6] mm/swap, memcg: Introduce swap tiers for cgroup based swap control
2026-07-13 15:50 ` [PATCH v10 0/6] mm/swap, memcg: Introduce swap tiers for cgroup based swap control Yosry Ahmed
2026-07-13 15:57 ` Youngjun Park
@ 2026-07-13 17:05 ` Chris Li
1 sibling, 0 replies; 20+ messages in thread
From: Chris Li @ 2026-07-13 17:05 UTC (permalink / raw)
To: Yosry Ahmed
Cc: Youngjun Park, akpm, linux-mm, cgroups, linux-kernel, kasong,
hannes, mhocko, roman.gushchin, shakeel.butt, muchun.song,
shikemeng, baoquan.he, baohua, joshua.hahnjy, gunho.lee,
taejoon.song, hyungjun.cho, baver.bae, her0gyugyu
On Mon, Jul 13, 2026 at 8:50 AM Yosry Ahmed <yosry@kernel.org> wrote:
>
> On Sun, Jul 12, 2026 at 7:57 PM Youngjun Park <youngjun.park@lge.com> wrote:
> >
> > This is the v10 series of the swap tier patchset.
> >
> > v10 folds in the Sashiko review fixes for the selftests added in v9 and
> > rebases onto the current mm-new. There are no functional changes to the
> > core swap or memcg code since v9; see the changelog for details.
> >
> > For context, the bulk of the series is unchanged since v8, with great thanks
> > to Shakeel Butt and Yosry for the reviews and discussions [1] that shaped it.
> > The main change in v8 was the interface change to use memory.swap.tiers.max
> > with '0' (disable) and 'max' (enable) values. This mechanism was suggested
> > by Shakeel and Yosry.
> >
> > This change allows for future extensions to control swap between tiers and
> > aligns better with existing memcg interfaces. It is confined to patch #3's
> > user-facing interface; internally, patch #3 still uses the existing mask
> > processing method, which is implementation-efficient.
> >
> > We also discussed tier extensions. Thanks to Yosry, Nhat and Shakeel for their
> > valuable feedback.
> >
> > Here is a brief summary of our tentative conclusions. Please correct me
> > if anything is misrepresented (details in references):
> >
> > * Zswap tiering [2]:
> > Zswap can itself be a tier (typically the fastest one). But, until vswap lands,
> > zswap cannot be the only allowed tier,
> > since it still needs a physical device for allocation;
> > that restriction can be lifted once vswap is supported.
>
> Does this series support zswap being a tier? I cannot find any mention
> of zswap in the patches.
In case I wasn't clear. As it is, zswap is not part of the tier and will not be.
Chris
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH v10 0/6] mm/swap, memcg: Introduce swap tiers for cgroup based swap control
2026-07-13 17:02 ` Chris Li
@ 2026-07-13 17:11 ` Yosry Ahmed
2026-07-13 18:34 ` Chris Li
0 siblings, 1 reply; 20+ messages in thread
From: Yosry Ahmed @ 2026-07-13 17:11 UTC (permalink / raw)
To: Chris Li
Cc: Youngjun Park, akpm, linux-mm, cgroups, linux-kernel, kasong,
hannes, mhocko, roman.gushchin, shakeel.butt, muchun.song,
shikemeng, baoquan.he, baohua, joshua.hahnjy, gunho.lee,
taejoon.song, hyungjun.cho, baver.bae, her0gyugyu
On Mon, Jul 13, 2026 at 10:03 AM Chris Li <chrisl@kernel.org> wrote:
>
> On Mon, Jul 13, 2026 at 9:01 AM Yosry Ahmed <yosry@kernel.org> wrote:
> > > My plan is to land the swap tier infrastructure together with the
> > > first use case (cgroup-based swap control) first, and then follow
> > > up with zswap tier support in a subsequent series, continuing the
> > > discussions we've had above.
> > > (I mentioned on cover letter, right above the overview section)
> > >
> > > Does that approach sound reasonable to you?
> >
> > How does swap tiering work with zswap in the current series? I assume
> > zswap is just enabled for all devices in all tiers? I wonder if
>
> Zswap is not part of the tiers exactly because it sits in front of all
> swap devices (tiers) and uses a different control to enable or disable
> it.
> Let's not combine these two; let zswap use its own existing cgroup
> control interface.
>
> > introducing zswap as a tier after the fact changes user-visible
> > behavior. I guess if zswap will be introduced with a default "max"
> > value it will more-or-less be the same behavior, but I would check all
> > user-visible behaviors related to zswap (e.g. interaction with other
> > zswap interfaces) to make sure nothing breaks or changes in a
> > meaningful way when zswap is introduced as a tier later.
>
> Zswap will not be introduced as a tier. The existing user interface
> makes zswap not exactly compatible with the tier ordering because it
> sits in front of every swapfile. If we change that, we break the user
> interface. I suggest we keep zswap working as it is now.
The goal from making zswap a swap tier is to have a single framework
to configure swapping for a cgroup, instead of configuring zswap
separately. Yes, zswap currently sits in front of all swap
devices/tiers, but we are heading in the direction of changing that
such that zswap is standalone, at which point it becomes more
obviously a swap tier. If you want us to wait until that happens
before adding zswap as a tier, I don't necessarily object, but I want
to make sure that nothing will break if we add zswap as a tier later.
An advantage of adding zswap as a tier right away is the proactive
writeback use case. It naturally fits in the tiering framework as
proactive demotion between swap tiers, which I expect may be useful in
non-zswap use cases as well. Without zswap as a tier, we'll have to
use a different interface for proactive writeback, and then if/when
zswap becomes a tier, we'll have multiple ways to do proactive
writeback which isn't ideal.
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH v10 0/6] mm/swap, memcg: Introduce swap tiers for cgroup based swap control
2026-07-13 17:11 ` Yosry Ahmed
@ 2026-07-13 18:34 ` Chris Li
2026-07-13 18:37 ` Yosry Ahmed
0 siblings, 1 reply; 20+ messages in thread
From: Chris Li @ 2026-07-13 18:34 UTC (permalink / raw)
To: Yosry Ahmed
Cc: Youngjun Park, akpm, linux-mm, cgroups, linux-kernel, kasong,
hannes, mhocko, roman.gushchin, shakeel.butt, muchun.song,
shikemeng, baoquan.he, baohua, joshua.hahnjy, gunho.lee,
taejoon.song, hyungjun.cho, baver.bae, her0gyugyu
On Mon, Jul 13, 2026 at 10:11 AM Yosry Ahmed <yosry@kernel.org> wrote:
>
> On Mon, Jul 13, 2026 at 10:03 AM Chris Li <chrisl@kernel.org> wrote:
> >
> > On Mon, Jul 13, 2026 at 9:01 AM Yosry Ahmed <yosry@kernel.org> wrote:
> > > > My plan is to land the swap tier infrastructure together with the
> > > > first use case (cgroup-based swap control) first, and then follow
> > > > up with zswap tier support in a subsequent series, continuing the
> > > > discussions we've had above.
> > > > (I mentioned on cover letter, right above the overview section)
> > > >
> > > > Does that approach sound reasonable to you?
> > >
> > > How does swap tiering work with zswap in the current series? I assume
> > > zswap is just enabled for all devices in all tiers? I wonder if
> >
> > Zswap is not part of the tiers exactly because it sits in front of all
> > swap devices (tiers) and uses a different control to enable or disable
> > it.
> > Let's not combine these two; let zswap use its own existing cgroup
> > control interface.
> >
> > > introducing zswap as a tier after the fact changes user-visible
> > > behavior. I guess if zswap will be introduced with a default "max"
> > > value it will more-or-less be the same behavior, but I would check all
> > > user-visible behaviors related to zswap (e.g. interaction with other
> > > zswap interfaces) to make sure nothing breaks or changes in a
> > > meaningful way when zswap is introduced as a tier later.
> >
> > Zswap will not be introduced as a tier. The existing user interface
> > makes zswap not exactly compatible with the tier ordering because it
> > sits in front of every swapfile. If we change that, we break the user
> > interface. I suggest we keep zswap working as it is now.
>
> The goal from making zswap a swap tier is to have a single framework
> to configure swapping for a cgroup, instead of configuring zswap
> separately. Yes, zswap currently sits in front of all swap
> devices/tiers, but we are heading in the direction of changing that
> such that zswap is standalone, at which point it becomes more
> obviously a swap tier. If you want us to wait until that happens
> before adding zswap as a tier, I don't necessarily object, but I want
> to make sure that nothing will break if we add zswap as a tier later.
I'm afraid your zswap user interface will have to break. I don't see a
way around breaking your zswap user interface to fit the swap tiering.
Once we move to the swap tier world, I don't think we should continue
using zswap.writeback to control the tier write back behavior. We will
need to rethink this new world.
> An advantage of adding zswap as a tier right away is the proactive
> writeback use case. It naturally fits in the tiering framework as
> proactive demotion between swap tiers, which I expect may be useful in
> non-zswap use cases as well. Without zswap as a tier, we'll have to
> use a different interface for proactive writeback, and then if/when
> zswap becomes a tier, we'll have multiple ways to do proactive
> writeback which isn't ideal.
I am looking forward to abstracting a more common write back behavior
in the swap tier world. The classic zswap behavior will be preserved.
Chris
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH v10 0/6] mm/swap, memcg: Introduce swap tiers for cgroup based swap control
2026-07-13 18:34 ` Chris Li
@ 2026-07-13 18:37 ` Yosry Ahmed
2026-07-13 19:38 ` Chris Li
0 siblings, 1 reply; 20+ messages in thread
From: Yosry Ahmed @ 2026-07-13 18:37 UTC (permalink / raw)
To: Chris Li
Cc: Youngjun Park, akpm, linux-mm, cgroups, linux-kernel, kasong,
hannes, mhocko, roman.gushchin, shakeel.butt, muchun.song,
shikemeng, baoquan.he, baohua, joshua.hahnjy, gunho.lee,
taejoon.song, hyungjun.cho, baver.bae, her0gyugyu
> > > Zswap will not be introduced as a tier. The existing user interface
> > > makes zswap not exactly compatible with the tier ordering because it
> > > sits in front of every swapfile. If we change that, we break the user
> > > interface. I suggest we keep zswap working as it is now.
> >
> > The goal from making zswap a swap tier is to have a single framework
> > to configure swapping for a cgroup, instead of configuring zswap
> > separately. Yes, zswap currently sits in front of all swap
> > devices/tiers, but we are heading in the direction of changing that
> > such that zswap is standalone, at which point it becomes more
> > obviously a swap tier. If you want us to wait until that happens
> > before adding zswap as a tier, I don't necessarily object, but I want
> > to make sure that nothing will break if we add zswap as a tier later.
>
> I'm afraid your zswap user interface will have to break. I don't see a
> way around breaking your zswap user interface to fit the swap tiering.
> Once we move to the swap tier world, I don't think we should continue
> using zswap.writeback to control the tier write back behavior. We will
> need to rethink this new world.
I wasn't talking about the existing zswap interfaces. I want to make
sure that if we introduce tiering initially without zswap as a tier,
then add zswap as a tier, the semantics of tiering and user-visible
zswap behavior doesn't break.
That being said, the existing zswap interfaces don't have to break
with tiering, why do they? We may end up with redundant interfaces,
which is unfortunate, and we can work to deprecate some of them over
time. But I don't see why we have to break them?
>
> > An advantage of adding zswap as a tier right away is the proactive
> > writeback use case. It naturally fits in the tiering framework as
> > proactive demotion between swap tiers, which I expect may be useful in
> > non-zswap use cases as well. Without zswap as a tier, we'll have to
> > use a different interface for proactive writeback, and then if/when
> > zswap becomes a tier, we'll have multiple ways to do proactive
> > writeback which isn't ideal.
>
> I am looking forward to abstracting a more common write back behavior
> in the swap tier world. The classic zswap behavior will be preserved.
Right, but this requires zswap being a tier, which you seem to be opposed to :)
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH v10 0/6] mm/swap, memcg: Introduce swap tiers for cgroup based swap control
2026-07-13 18:37 ` Yosry Ahmed
@ 2026-07-13 19:38 ` Chris Li
2026-07-13 19:57 ` Yosry Ahmed
0 siblings, 1 reply; 20+ messages in thread
From: Chris Li @ 2026-07-13 19:38 UTC (permalink / raw)
To: Yosry Ahmed
Cc: Youngjun Park, akpm, linux-mm, cgroups, linux-kernel, kasong,
hannes, mhocko, roman.gushchin, shakeel.butt, muchun.song,
shikemeng, baoquan.he, baohua, joshua.hahnjy, gunho.lee,
taejoon.song, hyungjun.cho, baver.bae, her0gyugyu
On Mon, Jul 13, 2026 at 11:38 AM Yosry Ahmed <yosry@kernel.org> wrote:
>
> > > > Zswap will not be introduced as a tier. The existing user interface
> > > > makes zswap not exactly compatible with the tier ordering because it
> > > > sits in front of every swapfile. If we change that, we break the user
> > > > interface. I suggest we keep zswap working as it is now.
> > >
> > > The goal from making zswap a swap tier is to have a single framework
> > > to configure swapping for a cgroup, instead of configuring zswap
> > > separately. Yes, zswap currently sits in front of all swap
> > > devices/tiers, but we are heading in the direction of changing that
> > > such that zswap is standalone, at which point it becomes more
> > > obviously a swap tier. If you want us to wait until that happens
> > > before adding zswap as a tier, I don't necessarily object, but I want
> > > to make sure that nothing will break if we add zswap as a tier later.
> >
> > I'm afraid your zswap user interface will have to break. I don't see a
> > way around breaking your zswap user interface to fit the swap tiering.
> > Once we move to the swap tier world, I don't think we should continue
> > using zswap.writeback to control the tier write back behavior. We will
> > need to rethink this new world.
>
> I wasn't talking about the existing zswap interfaces. I want to make
> sure that if we introduce tiering initially without zswap as a tier,
> then add zswap as a tier, the semantics of tiering and user-visible
> zswap behavior doesn't break.
No, the user visible part of zswap must break because zswap currently
sits in front of every swapfile.
I don't see any other way around it.
If you do know how zswap can interact with swap.tiers without breaking
the user interface, make a formal proposal and lay out all the
details. I did that exercise myself and I my conclusion is that it is
better to accept zswap is the classic behavior without burdening the
unified swap tier too much.
> That being said, the existing zswap interfaces don't have to break
> with tiering, why do they? We may end up with redundant interfaces,
Because zswap does not have its own swap device, it borrows the swap
slot from the underlying swap device. That behavior is unique to zswap
and none of the other swap tiers have that.
> which is unfortunate, and we can work to deprecate some of them over
> time. But I don't see why we have to break them?
I don't want to special case the zswap for from the swap tiers.
> > > An advantage of adding zswap as a tier right away is the proactive
> > > writeback use case. It naturally fits in the tiering framework as
> > > proactive demotion between swap tiers, which I expect may be useful in
> > > non-zswap use cases as well. Without zswap as a tier, we'll have to
> > > use a different interface for proactive writeback, and then if/when
> > > zswap becomes a tier, we'll have multiple ways to do proactive
> > > writeback which isn't ideal.
> >
> > I am looking forward to abstracting a more common write back behavior
> > in the swap tier world. The classic zswap behavior will be preserved.
>
> Right, but this requires zswap being a tier, which you seem to be opposed to :)
It does not need to as far as I can tell. People who want classic
zswap can use zswap as it is. I am thinking the swap ops provide some
interface for classic zswap to use, but zswap itself is not a tier
because it does not own swap devices.
Chris
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH v10 0/6] mm/swap, memcg: Introduce swap tiers for cgroup based swap control
2026-07-13 19:38 ` Chris Li
@ 2026-07-13 19:57 ` Yosry Ahmed
2026-07-13 21:49 ` Chris Li
0 siblings, 1 reply; 20+ messages in thread
From: Yosry Ahmed @ 2026-07-13 19:57 UTC (permalink / raw)
To: Chris Li
Cc: Youngjun Park, akpm, linux-mm, cgroups, linux-kernel, kasong,
hannes, mhocko, roman.gushchin, shakeel.butt, muchun.song,
shikemeng, baoquan.he, baohua, joshua.hahnjy, gunho.lee,
taejoon.song, hyungjun.cho, baver.bae, her0gyugyu
On Mon, Jul 13, 2026 at 12:39 PM Chris Li <chrisl@kernel.org> wrote:
>
> On Mon, Jul 13, 2026 at 11:38 AM Yosry Ahmed <yosry@kernel.org> wrote:
> >
> > > > > Zswap will not be introduced as a tier. The existing user interface
> > > > > makes zswap not exactly compatible with the tier ordering because it
> > > > > sits in front of every swapfile. If we change that, we break the user
> > > > > interface. I suggest we keep zswap working as it is now.
> > > >
> > > > The goal from making zswap a swap tier is to have a single framework
> > > > to configure swapping for a cgroup, instead of configuring zswap
> > > > separately. Yes, zswap currently sits in front of all swap
> > > > devices/tiers, but we are heading in the direction of changing that
> > > > such that zswap is standalone, at which point it becomes more
> > > > obviously a swap tier. If you want us to wait until that happens
> > > > before adding zswap as a tier, I don't necessarily object, but I want
> > > > to make sure that nothing will break if we add zswap as a tier later.
> > >
> > > I'm afraid your zswap user interface will have to break. I don't see a
> > > way around breaking your zswap user interface to fit the swap tiering.
> > > Once we move to the swap tier world, I don't think we should continue
> > > using zswap.writeback to control the tier write back behavior. We will
> > > need to rethink this new world.
> >
> > I wasn't talking about the existing zswap interfaces. I want to make
> > sure that if we introduce tiering initially without zswap as a tier,
> > then add zswap as a tier, the semantics of tiering and user-visible
> > zswap behavior doesn't break.
>
> No, the user visible part of zswap must break because zswap currently
> sits in front of every swapfile.
> I don't see any other way around it.
If zswap becomes usable independent from a swapfile, it's mostly
transparent to current users.
> If you do know how zswap can interact with swap.tiers without breaking
> the user interface, make a formal proposal and lay out all the
> details. I did that exercise myself and I my conclusion is that it is
> better to accept zswap is the classic behavior without burdening the
> unified swap tier too much.
Today pages go to zswap, and when the limit is hit they get written
back to a swap device. If zswap is a separate tier, pages will still
go to zswap and then when the limit is hit they get written back to a
swap device. In both cases, zswap writeback can be disabled.
Can you share the findings from your exercise, and why zswap being a
tier is a burden? Any specific examples?
>
> > That being said, the existing zswap interfaces don't have to break
> > with tiering, why do they? We may end up with redundant interfaces,
>
> Because zswap does not have its own swap device, it borrows the swap
> slot from the underlying swap device. That behavior is unique to zswap
> and none of the other swap tiers have that.
Yes, but we are heading in the direction of removing that restriction.
But I also don't see the connection between that and the current
interfaces breaking. Do you have any concrete examples?
>
> > which is unfortunate, and we can work to deprecate some of them over
> > time. But I don't see why we have to break them?
>
> I don't want to special case the zswap for from the swap tiers.
Neither do I, but I fail to see where the special casing is, beyond
the fact that zswap is not a swapfile but an in-memory swap.
> > > > An advantage of adding zswap as a tier right away is the proactive
> > > > writeback use case. It naturally fits in the tiering framework as
> > > > proactive demotion between swap tiers, which I expect may be useful in
> > > > non-zswap use cases as well. Without zswap as a tier, we'll have to
> > > > use a different interface for proactive writeback, and then if/when
> > > > zswap becomes a tier, we'll have multiple ways to do proactive
> > > > writeback which isn't ideal.
> > >
> > > I am looking forward to abstracting a more common write back behavior
> > > in the swap tier world. The classic zswap behavior will be preserved.
> >
> > Right, but this requires zswap being a tier, which you seem to be opposed to :)
>
> It does not need to as far as I can tell. People who want classic
> zswap can use zswap as it is. I am thinking the swap ops provide some
> interface for classic zswap to use, but zswap itself is not a tier
> because it does not own swap devices.
A swap tier represents a class of swap devices or swap files with
specific characteristics. Zswap satisfies that, although it's not
really a swap device/file.
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH v10 0/6] mm/swap, memcg: Introduce swap tiers for cgroup based swap control
2026-07-13 19:57 ` Yosry Ahmed
@ 2026-07-13 21:49 ` Chris Li
0 siblings, 0 replies; 20+ messages in thread
From: Chris Li @ 2026-07-13 21:49 UTC (permalink / raw)
To: Yosry Ahmed
Cc: Youngjun Park, akpm, linux-mm, cgroups, linux-kernel, kasong,
hannes, mhocko, roman.gushchin, shakeel.butt, muchun.song,
shikemeng, baoquan.he, baohua, joshua.hahnjy, gunho.lee,
taejoon.song, hyungjun.cho, baver.bae, her0gyugyu
On Mon, Jul 13, 2026 at 12:57 PM Yosry Ahmed <yosry@kernel.org> wrote:
>
> On Mon, Jul 13, 2026 at 12:39 PM Chris Li <chrisl@kernel.org> wrote:
> >
> > On Mon, Jul 13, 2026 at 11:38 AM Yosry Ahmed <yosry@kernel.org> wrote:
> > >
> > > > > > Zswap will not be introduced as a tier. The existing user interface
> > > > > > makes zswap not exactly compatible with the tier ordering because it
> > > > > > sits in front of every swapfile. If we change that, we break the user
> > > > > > interface. I suggest we keep zswap working as it is now.
> > > > >
> > > > > The goal from making zswap a swap tier is to have a single framework
> > > > > to configure swapping for a cgroup, instead of configuring zswap
> > > > > separately. Yes, zswap currently sits in front of all swap
> > > > > devices/tiers, but we are heading in the direction of changing that
> > > > > such that zswap is standalone, at which point it becomes more
> > > > > obviously a swap tier. If you want us to wait until that happens
> > > > > before adding zswap as a tier, I don't necessarily object, but I want
> > > > > to make sure that nothing will break if we add zswap as a tier later.
> > > >
> > > > I'm afraid your zswap user interface will have to break. I don't see a
> > > > way around breaking your zswap user interface to fit the swap tiering.
> > > > Once we move to the swap tier world, I don't think we should continue
> > > > using zswap.writeback to control the tier write back behavior. We will
> > > > need to rethink this new world.
> > >
> > > I wasn't talking about the existing zswap interfaces. I want to make
> > > sure that if we introduce tiering initially without zswap as a tier,
> > > then add zswap as a tier, the semantics of tiering and user-visible
> > > zswap behavior doesn't break.
> >
> > No, the user visible part of zswap must break because zswap currently
> > sits in front of every swapfile.
> > I don't see any other way around it.
>
> If zswap becomes usable independent from a swapfile, it's mostly
> transparent to current users.
>
> > If you do know how zswap can interact with swap.tiers without breaking
> > the user interface, make a formal proposal and lay out all the
> > details. I did that exercise myself and I my conclusion is that it is
> > better to accept zswap is the classic behavior without burdening the
> > unified swap tier too much.
>
> Today pages go to zswap, and when the limit is hit they get written
> back to a swap device. If zswap is a separate tier, pages will still
> go to zswap and then when the limit is hit they get written back to a
> swap device. In both cases, zswap writeback can be disabled.
>
> Can you share the findings from your exercise, and why zswap being a
> tier is a burden? Any specific examples?
It is all good when you have only zswap and just one other tier of swap device.
As soon as you add one more, e.g. zswap -> SSD -> HDD will make things
complicate when zswap using the HDD slot write back direclty to HDD.
Anyway, I find that this kind of highlevel hand waving discussion of
pros and cons usually doesn't produce the useful outcome. If you have
a detailed solution for how zswap works with multiple layers of swap
tiers, Laying out all the details and even better, providing some
incremental patches, is the better way to proceed.
> > > That being said, the existing zswap interfaces don't have to break
> > > with tiering, why do they? We may end up with redundant interfaces,
> >
> > Because zswap does not have its own swap device, it borrows the swap
> > slot from the underlying swap device. That behavior is unique to zswap
> > and none of the other swap tiers have that.
>
> Yes, but we are heading in the direction of removing that restriction.
> But I also don't see the connection between that and the current
> interfaces breaking. Do you have any concrete examples?
See above. This topic has already seen a lot of heated discussion.
Show me an incrementally mergeable patch; that would be a better way
to move forward.
Chris
^ permalink raw reply [flat|nested] 20+ messages in thread
end of thread, other threads:[~2026-07-13 21:50 UTC | newest]
Thread overview: 20+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-13 2:56 [PATCH v10 0/6] mm/swap, memcg: Introduce swap tiers for cgroup based swap control Youngjun Park
2026-07-13 2:56 ` [PATCH v10 1/6] mm: swap: introduce swap tier infrastructure Youngjun Park
2026-07-13 2:56 ` [PATCH v10 2/6] mm: swap: associate swap devices with tiers Youngjun Park
2026-07-13 14:28 ` Usama Arif
2026-07-13 2:56 ` [PATCH v10 3/6] mm: memcontrol: add interface for swap tier selection Youngjun Park
2026-07-13 2:56 ` [PATCH v10 4/6] mm: swap: filter swap allocation by memcg tier mask Youngjun Park
2026-07-13 2:56 ` [PATCH v10 5/6] selftests/mm: add a swap tier configuration test Youngjun Park
2026-07-13 2:56 ` [PATCH v10 6/6] selftests/cgroup: add a swap tier routing test Youngjun Park
2026-07-13 15:50 ` [PATCH v10 0/6] mm/swap, memcg: Introduce swap tiers for cgroup based swap control Yosry Ahmed
2026-07-13 15:57 ` Youngjun Park
2026-07-13 16:01 ` Yosry Ahmed
2026-07-13 16:22 ` Youngjun Park
2026-07-13 17:02 ` Chris Li
2026-07-13 17:11 ` Yosry Ahmed
2026-07-13 18:34 ` Chris Li
2026-07-13 18:37 ` Yosry Ahmed
2026-07-13 19:38 ` Chris Li
2026-07-13 19:57 ` Yosry Ahmed
2026-07-13 21:49 ` Chris Li
2026-07-13 17:05 ` Chris Li
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox