kerf: CPU/NUMA topology-aware resource allocation - #12
Conversation
|
|
||
| total_bytes = memory_pool_base + memory_pool_bytes | ||
| pool_list.sort(key=lambda pool: pool.base) | ||
| memory_pool_base = pool_list[0].base |
There was a problem hiding this comment.
Hmm, why only [0]? This somehow implies that we have all pools being continuous. What if they are not?
Shall we add an extra-check if the pools are continuous, and reject them if they are not?
There was a problem hiding this comment.
Allocation never assumes the pools are contiguous: find_available_memory_base walks get_pools() and first-fits inside each pool separately, and validate_memory_allocation requires a region to sit entirely within a single pool. pools[0].base only feeds the legacy memory-base/memory-bytes envelope kept for single-pool kernel compatibility and display; the exact ranges travel in the memory-pools node. Rejecting non-contiguous pools would break per-NUMA pools, since lazy_cma allocates them independently and they are rarely adjacent. The real caveat is a pools-unaware consumer that reads only memory-base/memory-bytes from a multi-pool tree; multi-pool baselines need a pools-aware kernel either way.
| # boundaries, so no placement policy is attached unless asked for. | ||
| if is_count: | ||
| # Allocate CPUs automatically from available pool with topology awareness | ||
| effective_affinity = cpu_affinity or "compact" |
There was a problem hiding this comment.
We default to compact here, but that would require that Numa nodes are supplied on the command line already.
At least that is how I read _allocate_compact()
There was a problem hiding this comment.
_allocate_compact only takes the NUMA branch when numa_nodes is truthy; without it, control falls through to _find_consecutive_cpus over the whole available list and then available[:count]. So the compact default works with no NUMA nodes on the command line, it simply is not NUMA aware in that case. A possible follow-up is to prefer a single node when topology is known even without an explicit node list.
| except libfdt.FdtException: | ||
| pass | ||
|
|
||
| return InstanceResources( |
There was a problem hiding this comment.
Confirmed and fixed in d283a96: the baseline DTB writer never emitted numa-nodes, cpu-affinity or memory-policy and the DTB parsers never read them, so they were lost on every baseline round trip even though the overlay carried them to the kernel. The extractor now writes all three for each instance and both DTB resource parsers restore them.
|
|
||
| # Create instance resources with topology settings | ||
| uring_enabled = uring or uring_sq_entries is not None or uring_cq_entries is not None or uring_shim_pages is not None | ||
| resources = InstanceResources( |
There was a problem hiding this comment.
We store the affinity and nodes info here -> and then drop it later in the parser
There was a problem hiding this comment.
Fixed in d283a96 together with the parser side: the baseline writer now emits numa-nodes, cpu-affinity and memory-policy, and the DTB parsers read them back, so what create stores here is no longer dropped on reload.
There was a problem hiding this comment.
Are we sure this is going to parse :
cpus = <0x0 0x80 0x0 0x82>
There was a problem hiding this comment.
Good catch, it did not parse: int() without a base raised ValueError on hex tokens, and even with hex support the 64-bit cell pairs would have been misread as four separate IDs. Fixed in d283a96 with parse_cpu_id_cells, which accepts decimal and hex cells and decodes an even-length list whose high (even-index) cells are all zero as 64-bit pairs, mirroring unpack_cpu_ids on the binary side. Hand-written single-cell lists like <2 3 4 5> still parse as before.
There was a problem hiding this comment.
Maybe we can re-use the new read_logical_to_physical_cpu_map here and simplify this code.
There was a problem hiding this comment.
Done in d283a96. This also fixed a semantic bug: the old code scraped 'physical id' from /proc/cpuinfo, which is the socket ID, so on any multi-core host it warned that valid APIC IDs did not exist. Validation now checks the DTB IDs against set(read_logical_to_physical_cpu_map().values()), and the stale logical-processor check is gone.
The topology models, allocation policies and validators already existed but were dead code in practice: the baseline DTB generator never emitted the topology section, so every create/update reading state back from the kernel saw no topology, and nothing ever discovered it from the host. Make the support real: - Emit the topology section (NUMA nodes, per-node memory range, CPUs as 64-bit physical ID cells, distance matrix as <target distance> pairs, memory type) and device numa-node into the baseline DTB, and parse them back, so topology survives the kernel round trip. - Add kerf/topology.py to discover the host topology at kerf init time: NUMA nodes and distances from /sys/devices/system/node, per-node memory ranges from /proc/zoneinfo, and PCI device locality from sysfs. Sysfs is keyed by logical CPU while kerf speaks physical APIC IDs, so discovery translates through the processor/apicid pairs in /proc/cpuinfo. - Fix DTS parsing bugs that broke hand-written topology sections: the topology/numa-nodes/cores regexes could not handle nested braces (only the first node parsed), comments inside multi-line property values broke value parsing, and the pool memory-base regex could match a NUMA node's memory-base instead. - Keep manual allocation authoritative: explicit --cpus no longer gets a compact affinity policy attached implicitly, so deliberate topology-crossing layouts do not accumulate spurious warnings. Auto-allocation (--cpu-count) still defaults to compact. Topology violations remain warnings; only impossible requests are errors. - Fold the topology documentation into README.md, replacing the stale docs/CPU_NUMA_TOPOLOGY.md, and update the NUMA examples to the supported format. Instance memory is still allocated first-fit from a single pool; NUMA-aware per-node pools via /dev/lazy_cma's node parameter are the next step.
Instance memory used to be first-fit from one contiguous pool allocated
with no node preference, so on multi-socket hosts all instances ended up
on whichever node happened to have contiguous memory, and memory-policy
was recorded but never influenced placement.
Introduce per-node memory pools:
- kerf init accepts per-node sizes ("memory=8GB@0,8GB@1"), allocating
one pool per NUMA node through lazy_cma's node parameter, which was
wired up in the kernel module but never used. A single anonymous size
keeps the exact legacy behavior and DTB layout. On re-init, all
reserved pools in /proc/iomem are rediscovered and matched to NUMA
nodes via the discovered topology's memory ranges.
- The pool layout round-trips through the baseline as a memory-pools
section (ignored by the kernel), parsed from both DTB and DTS. The
legacy memory-base/memory-bytes properties remain as the envelope for
compatibility; allocation logic operates on the pool list, with a
synthesized single pool for old baselines.
- Allocation first-fits within each pool, never spanning the gap
between pools, and validation requires an instance region to lie
entirely inside one pool. The validator's iomem cross-check now
verifies every configured pool against all reserved regions.
- kerf create implements the placement policies: "local" hard-requires
a pool on the same node as the instance CPUs, "bind" hard-requires a
pool on the requested NUMA nodes, and with no policy kerf prefers a
CPU-local pool and silently falls back to any pool. An explicit
memory base remains authoritative and only gets locality warnings.
"interleave" stays unimplemented since instances receive a single
contiguous region; true interleaving needs kernel support for
multiple regions per instance.
Also fix a leftover from the logical-CPU-numbering era: the validator
rejected instance CPUs with id >= total, which falsely fails sparse
physical APIC IDs; it now checks membership in the hardware CPU set.
- parser: return plain lists from the memory-pool parsers and convert to None at the construction sites, so pylint can prove iterability (not-an-iterable) - lazy_cma, console, init: convert str.format() calls to f-strings (consider-using-f-string) - tests: underscore-name unused fake-interface arguments and disable redefined-outer-name for pytest fixtures (unused-argument, redefined-outer-name)
Preserve numa-nodes, cpu-affinity and memory-policy across baseline DTB round trips: the extractor now writes them for each instance and both DTB resource parsers read them back, so placement settings no longer revert to None after a baseline reload. Parse DTS cpus cell lists with hex support and decode 64-bit cell pairs whose high cells are zero, matching unpack_cpu_ids, so a dtc round trip of the 64-bit encoding parses correctly. Validate DTB physical CPU IDs against the system APIC ID set from read_logical_to_physical_cpu_map instead of socket IDs scraped from /proc/cpuinfo, which flagged valid APIC IDs on multi-core hosts. Signed-off-by: Cong Wang <cwang@multikernel.io>
d283a96 to
b6f9eb5
Compare
Closes #9
Summary
Makes kerf topology-aware end to end: the baseline records the host's real CPU/NUMA topology, auto-allocation places CPUs and memory for locality, and validation reports topology violations as warnings while keeping manual allocation fully authoritative.
Two commits:
1. Make NUMA topology support functional end to end
The topology models, allocation policies and validators already existed but were dead code: the baseline DTB generator never emitted the topology section, and nothing discovered topology from the host.
kerf initnow discovers the host topology automatically: NUMA nodes and distances from sysfs, per-node memory ranges from /proc/zoneinfo, PCI device locality from sysfsnuma_node. Sysfs is keyed by logical CPU while kerf speaks physical APIC IDs, so discovery translates through the processor/apicid pairs in /proc/cpuinfo.numa-node) round-trips through the baseline DTB, so every later create/update sees it. The kernel ignores these nodes.memory-basebeing shadowed by a NUMA node'smemory-base.2. Per-NUMA-node memory pools and policy-driven placement
kerf initaccepts per-node pool sizes (memory=8GB@0,8GB@1), allocating one pool per NUMA node through lazy_cma's previously unused node parameter. A single anonymous size keeps the exact legacy behavior and DTB layout. Existing pools in /proc/iomem are rediscovered on re-init and matched to nodes via the topology.memory-poolssection (kernel-ignored), parsed from both DTB and DTS.memory-policynow drives placement:localhard-requires a pool on the same node as the instance CPUs,bindhard-requires a pool on the requested nodes, no policy prefers a CPU-local pool with silent fallback. Explicitmemory-baseremains authoritative with warnings only.interleavestays unimplemented pending kernel support for multiple regions per instance.Testing
59 new tests (tests/test_topology.py, tests/test_memory_pools.py) covering DTB/DTS round-trips, sysfs discovery against fake trees, pool-aware allocation/validation, and CLI placement policies via dry-run. Full suite: 136 passed. Discovery and DTB round-trip also verified against a live host.
Known follow-ups
kerf updatereuses the pool-aware allocator but does not take placement policy flags yet.topology,memory-pools) verbatim; worth one check on multikernel hardware.