ktstr/workload/config/mempolicy.rs
1//! NUMA memory-placement policy types for the workload pipeline.
2//!
3//! Holds [`MemPolicy`] (the `MPOL_*` enum the worker process
4//! applies via `set_mempolicy(2)` after fork) and [`MpolFlags`]
5//! (the `MPOL_F_*` flag bag OR'd into the mode argument). Validation
6//! lives on [`MemPolicy::validate`] — each variant that takes a
7//! node set rejects empty sets with an actionable diagnostic.
8
9use std::collections::BTreeSet;
10
11/// NUMA memory placement policy for worker processes.
12///
13/// Applied via `set_mempolicy(2)` after fork, before the work loop.
14/// Maps to Linux `MPOL_*` constants. When `Default`, no syscall is
15/// made (inherits the parent's policy).
16///
17/// Optional [`MpolFlags`] modify behavior (e.g. `STATIC_NODES` to
18/// keep the nodemask absolute across cpuset changes).
19#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
20#[serde(rename_all = "snake_case")]
21pub enum MemPolicy {
22 /// Inherit the parent process's memory policy (no syscall).
23 #[default]
24 Default,
25 /// Allocate only from the specified NUMA nodes (`MPOL_BIND`).
26 Bind(BTreeSet<usize>),
27 /// Prefer allocations from the specified node, falling back to
28 /// others when the preferred node is full (`MPOL_PREFERRED`).
29 Preferred(usize),
30 /// Interleave allocations round-robin across the specified nodes
31 /// (`MPOL_INTERLEAVE`).
32 Interleave(BTreeSet<usize>),
33 /// Prefer the nearest node to the CPU where the allocation occurs
34 /// (`MPOL_LOCAL`). No nodemask.
35 Local,
36 /// Prefer allocations from any of the specified nodes, falling back
37 /// to others when all preferred nodes are full
38 /// (`MPOL_PREFERRED_MANY`, kernel 5.15+).
39 PreferredMany(BTreeSet<usize>),
40 /// Weighted interleave across the specified nodes. Page distribution
41 /// is proportional to per-node weights set via
42 /// `/sys/kernel/mm/mempolicy/weighted_interleave/nodeN`
43 /// (`MPOL_WEIGHTED_INTERLEAVE`, kernel 6.9+).
44 WeightedInterleave(BTreeSet<usize>),
45}
46
47/// Optional mode flags for `set_mempolicy(2)`.
48///
49/// OR'd into the mode argument. See `MPOL_F_*` in
50/// `include/uapi/linux/mempolicy.h`.
51#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
52#[serde(transparent)]
53pub struct MpolFlags(u32);
54
55impl MpolFlags {
56 /// No flags.
57 pub const NONE: Self = Self(0);
58 /// `MPOL_F_STATIC_NODES` (1 << 15): nodemask is absolute, not
59 /// remapped when the task's cpuset changes.
60 pub const STATIC_NODES: Self = Self(1 << 15);
61 /// `MPOL_F_RELATIVE_NODES` (1 << 14): nodemask is relative to
62 /// the task's current cpuset.
63 pub const RELATIVE_NODES: Self = Self(1 << 14);
64 /// `MPOL_F_NUMA_BALANCING` (1 << 13): enable NUMA balancing
65 /// optimization for this policy.
66 pub const NUMA_BALANCING: Self = Self(1 << 13);
67
68 /// Test-only raw-bit constructor. Lets unknown-bit guards
69 /// (e.g. `validate_mempolicy_cpuset` in src/scenario/ops/mod.rs)
70 /// be tested against bit patterns that are not reachable via
71 /// the documented `STATIC_NODES | RELATIVE_NODES |
72 /// NUMA_BALANCING` constants. Production callers must use the
73 /// named constants + `union` / `BitOr` so the model stays in
74 /// sync with the validator's known-bits mask.
75 #[cfg(test)]
76 pub(crate) const fn from_bits_for_test(bits: u32) -> Self {
77 Self(bits)
78 }
79
80 /// Combine two flag sets.
81 pub const fn union(self, other: Self) -> Self {
82 Self(self.0 | other.0)
83 }
84
85 /// Raw flag bits for passing to the syscall.
86 pub const fn bits(self) -> u32 {
87 self.0
88 }
89
90 /// Whether every bit in `other` is set in `self`.
91 ///
92 /// Set-theoretic, not syntactic: `contains(NONE)` returns `true`
93 /// for any `self` (vacuous truth — the empty set is a subset of
94 /// everything). Callers who want "has a non-empty intersection
95 /// with `other`" must compare `self.bits() & other.bits() != 0`
96 /// explicitly; using `contains` for that query silently returns
97 /// `true` when the operand is `NONE` regardless of `self`.
98 pub const fn contains(self, other: Self) -> bool {
99 (self.0 & other.0) == other.0
100 }
101}
102
103impl std::ops::BitOr for MpolFlags {
104 type Output = Self;
105 /// Delegates to [`MpolFlags::union`] so the bitwise-OR logic
106 /// lives in one place. `union` is `const fn` (usable in
107 /// const contexts like `const` initializers); `BitOr::bitor`
108 /// cannot currently be `const` on stable, so keeping both
109 /// entry points is necessary, but they must never diverge.
110 fn bitor(self, rhs: Self) -> Self {
111 self.union(rhs)
112 }
113}
114
115impl MemPolicy {
116 /// Construct a `Bind` policy from any iterator of NUMA node IDs.
117 ///
118 /// Accepts arrays, ranges, `Vec`, `BTreeSet`, or any `IntoIterator<Item = usize>`.
119 pub fn bind(nodes: impl IntoIterator<Item = usize>) -> Self {
120 MemPolicy::Bind(nodes.into_iter().collect())
121 }
122
123 /// Construct a `Preferred` policy for a single NUMA node.
124 pub const fn preferred(node: usize) -> Self {
125 MemPolicy::Preferred(node)
126 }
127
128 /// Construct an `Interleave` policy from any iterator of NUMA node IDs.
129 ///
130 /// Accepts arrays, ranges, `Vec`, `BTreeSet`, or any `IntoIterator<Item = usize>`.
131 pub fn interleave(nodes: impl IntoIterator<Item = usize>) -> Self {
132 MemPolicy::Interleave(nodes.into_iter().collect())
133 }
134
135 /// Construct a `PreferredMany` policy from any iterator of NUMA node IDs.
136 pub fn preferred_many(nodes: impl IntoIterator<Item = usize>) -> Self {
137 MemPolicy::PreferredMany(nodes.into_iter().collect())
138 }
139
140 /// Construct a `WeightedInterleave` policy from any iterator of NUMA node IDs.
141 pub fn weighted_interleave(nodes: impl IntoIterator<Item = usize>) -> Self {
142 MemPolicy::WeightedInterleave(nodes.into_iter().collect())
143 }
144
145 /// NUMA node IDs referenced by this policy.
146 ///
147 /// Returns the node set for `Bind`, `Interleave`, `PreferredMany`,
148 /// and `WeightedInterleave`, a single-element set for `Preferred`,
149 /// and an empty set for `Default`/`Local`.
150 pub fn node_set(&self) -> BTreeSet<usize> {
151 match self {
152 MemPolicy::Default | MemPolicy::Local => BTreeSet::new(),
153 MemPolicy::Bind(nodes)
154 | MemPolicy::Interleave(nodes)
155 | MemPolicy::PreferredMany(nodes)
156 | MemPolicy::WeightedInterleave(nodes) => nodes.clone(),
157 MemPolicy::Preferred(node) => [*node].into_iter().collect(),
158 }
159 }
160
161 /// Validate that this policy's node set is non-empty where required.
162 ///
163 /// Returns `Err` with a description when a node-set-bearing policy
164 /// has an empty set. Each diagnostic names the offending variant,
165 /// the constraint ("at least one NUMA node"), and the actionable
166 /// fix — both the matching constructor and the recommended
167 /// fallbacks for "I don't want node-restricted placement" — so the
168 /// operator sees the next mouse-click inline rather than having to
169 /// `cargo doc --open` to discover the constructor menu. Mirrors the
170 /// workers_pct rejection trailer at `WorkSpec::resolve_workers_pct`.
171 pub fn validate(&self) -> std::result::Result<(), String> {
172 match self {
173 MemPolicy::Default | MemPolicy::Local => Ok(()),
174 MemPolicy::Preferred(_) => Ok(()),
175 MemPolicy::Bind(nodes) if nodes.is_empty() => Err(
176 "Bind policy requires at least one NUMA node — \
177 use `MemPolicy::bind([node, ...])` with one or more node IDs, \
178 or `MemPolicy::Default` / `MemPolicy::Local` for non-bound placement"
179 .into(),
180 ),
181 MemPolicy::Interleave(nodes) if nodes.is_empty() => Err(
182 "Interleave policy requires at least one NUMA node — \
183 use `MemPolicy::interleave([node, ...])` with one or more node IDs, \
184 or `MemPolicy::Default` for unrestricted placement"
185 .into(),
186 ),
187 MemPolicy::PreferredMany(nodes) if nodes.is_empty() => Err(
188 "PreferredMany policy requires at least one NUMA node — \
189 use `MemPolicy::preferred_many([node, ...])` with one or more node IDs, \
190 or `MemPolicy::Preferred(node)` / `MemPolicy::Default` / `MemPolicy::Local`"
191 .into(),
192 ),
193 MemPolicy::WeightedInterleave(nodes) if nodes.is_empty() => Err(
194 "WeightedInterleave policy requires at least one NUMA node — \
195 use `MemPolicy::weighted_interleave([node, ...])` with one or more node IDs, \
196 or `MemPolicy::interleave([node, ...])` / `MemPolicy::Default` for unweighted placement"
197 .into(),
198 ),
199 _ => Ok(()),
200 }
201 }
202}