Skip to content

kerf init: reconcile the live pool instead of writing the baseline once - #17

Merged
congwang-mk merged 23 commits into
mainfrom
pool-reinit
Aug 22, 2026
Merged

kerf init: reconcile the live pool instead of writing the baseline once#17
congwang-mk merged 23 commits into
mainfrom
pool-reinit

Conversation

@congwang-mk

@congwang-mk congwang-mk commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Summary

kerf init can now be run any number of times. The first run writes the baseline; every later run reads the live pool back from the kernel, diffs it against the request, and applies the difference as a /resources overlay transaction. This pairs with the kernel series on multikernel-latest that makes the pool a set of kernel-owned NUMA-node chunks, addresses overlay fragments by target-path, and lets the pool grow, shrink and be torn down.

User-visible changes

  • kerf init --cpus=... --memory=SIZE is idempotent: re-running with a different request grows or shrinks the pool in place (CPUs, memory, PCI devices).
  • --memory takes SIZE or SIZE@N,... (8GB@0,8GB@1), mirroring the device-tree memory@N unit address. A plain SIZE is resolved by kerf to the NUMA node of the requested CPUs, so the kernel is always handed an explicit node.
  • kerf init --teardown returns every pool resource to the host; it refuses while instances exist and names them.
  • --dry-run prints the planned moves and why a pool cannot shrink further (only whole idle chunks go back).
  • kerf show lists every pool chunk with its base, size and node, and separates pool CPUs from the free subset.
  • The lazy_cma client is gone; the kernel allocates pool memory itself.

Device-tree contract (no backward compatibility)

  • Baseline: /resources { cpus; memory@N { size; numa-node-id }; devices }; memory-base/memory-bytes are rejected.
  • Read-back: cpus (members), cpus-available (free), memory@<base> { device_type; reg; numa-node-id } per chunk.
  • Overlays: every fragment carries target-path (/resources, /instances/<name>, /instances); mk,instance is gone; memory items are memory@N; the NUMA property is numa-node-id.

Internals

  • PoolMemoryRegion / per-node requested in the model; parser for both baseline shapes; compute_pool_diff(); OverlayGenerator.generate_pool_overlay(); one DeviceTreeManager.apply_dtbo() write path; multi-chunk /proc/iomem helpers; validation against the chunk set instead of a single range.
  • Examples migrated to the new baseline format.

Testing

  • Unit suite: 242 tests pass (python3 -m pytest tests -q), pylint clean on touched files.
  • End-to-end on the libvirt VM against the matching kernel: first init, idempotent re-run, grow to two chunks, shrink, instance create/load/exec on a two-chunk pool, a second create refused because the CPU is lent, re-init that would steal a running instance's CPU refused with the kernel's reason, --teardown refused while an instance exists, full teardown, re-init from scratch, SIZE@N and unpinned-node resolution (including the offline-pool-CPU fallback).

Follow-ups (not in this PR)

  • --teardown --dry-run does not warn about running instances.
  • Shrinking the CPU set while a CPU is lent still relies on the kernel's -EBUSY.
  • kerf update range math after a multi-region grow needs the kernel to publish per-region memory@ nodes under /instances/<name>.

Close #8

The kernel now owns pool memory as a list of contiguous chunks, each on
one NUMA node, and accepts per-node size requests instead of a single
donated range, so MemoryAllocation can no longer be a base and a size.
Model it as a list of PoolMemoryRegion (base, size, node) read back from
the kernel plus a node-to-bytes request map, keep memory_pool_base,
memory_pool_bytes and memory_pool_end as derived properties so the
existing consumers keep working, and convert every constructor site in
the tree and the tests.

Signed-off-by: Cong Wang <cwang@multikernel.io>
The root device tree now reports pool memory as memory@<base> nodes with
a reg pair and a numa-node-id, and a baseline request names per-node
sizes instead of a single base/bytes pair. Parse both shapes, keep cpus-
available as the free subset of the pool, reject the legacy memory-base
and memory-bytes properties, and emit the request shape from the
extractor so the baseline still round-trips through the parser.

Signed-off-by: Cong Wang <cwang@multikernel.io>
…ples

A NUMA topology node describes itself with memory-base and memory-size,
so scanning the whole /resources body for the legacy pool properties
rejected trees that were already migrated. Split the body into its own
properties and its direct children, check only the former, read pool
chunks only from depth-0 memory@ nodes, migrate the example baselines to
memory@N requests, reject a /resources that describes no memory at all,
and catch a lone memory-base on the DTB path.

Signed-off-by: Cong Wang <cwang@multikernel.io>
The baseline written to the kernel is a request, not a description of
memory the host already set aside, so the extractor emits one memory@N
node per requested node carrying a size and, when the node is explicit,
a numa-node-id, exactly the shape baseline.c parses. This commit adds
the test that pins that format; the emitter itself landed with the
parser change so the suite never saw a broken round-trip.

Signed-off-by: Cong Wang <cwang@multikernel.io>
…seline

Re-initializing the pool means moving resources between the host and the
pool rather than writing a new baseline, so kerf needs to know what
differs. Add compute_pool_diff(): set differences for CPUs and PCI
devices, and a per-node memory comparison that grows a node by the
missing amount or releases whole chunks when it holds too much,
preferring chunks with no instance allocation and never splitting one.
The request is the desired state, so memory on nodes it does not mention
counts as surplus.

Signed-off-by: Cong Wang <cwang@multikernel.io>
The most consequential behaviour of the pool diff, that a request naming
only some nodes releases the memory on every other node, had no test,
nor did the larger-first order in which idle chunks are returned. Add
both cases and state the desired-state rule in the docstring so a future
change cannot silently turn it into a merge.

Signed-off-by: Cong Wang <cwang@multikernel.io>
The kernel dropped the mk,instance property and now addresses every
overlay fragment by the standard target-path: /resources for the pool,
/instances/<name> for an instance and /instances for create and remove.
Generate pool transactions from a PoolDiff as a /resources fragment
whose operations read from the pool's point of view, convert the
instance update and create overlays to target-path, rename the memory
items to memory@N and the NUMA property to numa-node-id, and carry numa-
node-id on an instance's memory-add so a grow of a node-pinned instance
stays on its node.

Signed-off-by: Cong Wang <cwang@multikernel.io>
The write-and-check-the-transaction sequence lived in three copies, and
the iomem helpers still assumed a single pool line although the kernel
now registers one line per chunk. Collapse the copies into
DeviceTreeManager.apply_dtbo(), report every chunk and note which ones
still hold an allocation, and emit fragment unit-addresses in hex
because the kernel parses them base 16.

Signed-off-by: Cong Wang <cwang@multikernel.io>
The kernel refuses a baseline write once the pool holds resources, so
re-running kerf init failed with EBUSY. Read the pool back, diff it
against the request and apply the difference as a /resources overlay
transaction, leaving the baseline write for an empty pool only. The
--memory option now describes the request rather than a runtime
allocation, so the lazy_cma client is gone, and --teardown hands every
pool resource back to the host.

Signed-off-by: Cong Wang <cwang@multikernel.io>
…list

Before a pool exists the kernel still publishes a root device tree, and
it describes the host: the online CPUs land in cpus with no cpus-
available and no memory node, so reading it back raised a ParseError
that aborted the very first init, and the CPU list alone made an empty
pool look live. Treat an unreadable read-back as no pool yet and take
liveness from cpus-available, the pool chunks and the pool devices,
which only the pool branch of the kernel's tree emits; a teardown of an
already empty pool no longer writes a baseline the kernel would reject,
one that would strand instances names them instead of failing the
transaction, and a read-back that fails after the transaction landed is
a note rather than an error.

Signed-off-by: Cong Wang <cwang@multikernel.io>
kerf show printed a single pool base and size, which cannot describe a
pool made of several chunks on different nodes. List every chunk with
its base, size and node, show pool membership and the free subset of
CPUs separately now that the kernel reports both, hide the total and
host-reserved lines when the read-back carries no such numbers, and stop
the validation reporter from dividing by a zero total.

Signed-off-by: Cong Wang <cwang@multikernel.io>
The host drops a CPU from /proc/cpuinfo once the pool owns it, so every
re-init of a live pool rejected the very CPUs it was asked to keep. Read
the pool before building the requested tree and treat its CPUs as valid
APIC ids, and skip the post-teardown read-back that always failed
because an emptied pool has no memory@N left to report.

Signed-off-by: Cong Wang <cwang@multikernel.io>
The pool is a list of chunks, but validation still compared the read-
back against the first chunk in /proc/iomem and bounded instance memory
by base plus sum, so a two-chunk pool failed validation and kerf create
aborted before reaching the kernel. Compare the whole chunk set, check
that an instance region lies inside one chunk, stop picking a base in
kerf create since the kernel places instance memory itself (a base given
on the command line is still checked), and let an instance grow only
into the chunk it already sits in because the memory-add overlay names
an existing range.

Signed-off-by: Cong Wang <cwang@multikernel.io>
A transaction can hand back every chunk and keep the pool CPUs, so a
read-back with cpus-available but no memory@ node describes a real pool,
not a broken one; rejecting it made kerf init fall back to writing a
baseline, which the kernel refuses while it owns resources. Accept that
shape as a pool with no memory while still requiring a request baseline
to name its memory, since nothing else says how much pool to build.

Signed-off-by: Cong Wang <cwang@multikernel.io>
Only whole idle chunks go back to the host, so a request smaller than
every chunk leaves the pool as it is and the diff comes out empty, which
looked like success with no explanation. Report the surplus on the paths
that move nothing, the empty diff and --dry-run, the way the path that
applies a transaction already does, and give sizes coming from --input
the same page-alignment check as --memory since the kernel rejects the
rest either way.

Signed-off-by: Cong Wang <cwang@multikernel.io>
The root read-back publishes pool membership in cpus and the free
members in cpus-available, and carries no instances section. Deriving
free CPUs by subtracting the tree's instances from membership therefore
offered CPUs already lent to a running instance, and kerf create only
found out when the kernel refused the transaction; prefer cpus-available
whenever the tree carries it.

Signed-off-by: Cong Wang <cwang@multikernel.io>
Nothing here changes what the kernel is asked to do: name the pool
transaction file's contents for what they are, the target path of the
overlay's first fragment rather than an instance; make the device tree
lock public so callers stop reaching into a private method with a pylint
waiver; drop the DTB writers and the u64 reader that nothing calls, and
a hardware check whose message contradicted its condition; spell the
any-node NUMA id as pool_diff.ANY_NODE; say "No memory pool configured"
after a teardown instead of printing nothing; and name the example's
device NUMA property numa-node-id as everywhere else.

Signed-off-by: Cong Wang <cwang@multikernel.io>
The diff preferred idle chunks but still fell back to a chunk holding a
running instance when no idle chunk was small enough to cover the
surplus; the kernel refuses that chunk and fails the whole transaction,
so kerf init turned an unshrinkable pool into an error instead of the
note it already prints for that case. Only whole idle chunks go back, so
never propose a busy one.

Signed-off-by: Cong Wang <cwang@multikernel.io>
Switch the per-node syntax of --memory from nodeK:SIZE to SIZE@N,
matching the device-tree unit address convention and the kernel's own
memory@N read-back instead of inventing a bespoke node prefix; a plain
SIZE still means an unpinned request and the two forms cannot be mixed.

Signed-off-by: Cong Wang <cwang@multikernel.io>
A --memory size without an @n used to reach the kernel with no numa-
node-id, leaving the kernel to choose where the pool memory came from;
placement is policy and policy belongs in kerf. Resolve the node here,
from the NUMA node of the requested CPUs (the node they share, or the
lowest APIC id's node when they straddle nodes), falling back to the
node of the chunks the pool already holds when its CPUs are offline and
the host no longer places them, and to node 0 when the host reports no
topology at all; baselines read from --input get the same treatment, so
the kernel is always handed an explicit node.

Signed-off-by: Cong Wang <cwang@multikernel.io>
CI runs a pylint release that flags yielding elements one by one where
yield from applies and comparing a result against an empty dict instead of
testing its truthiness. Neither changes what the tests verify, so adopt the
suggested spellings to keep the lint gate green.

Signed-off-by: Cong Wang <cwang@multikernel.io>
@congwang-mk congwang-mk mentioned this pull request Aug 22, 2026
Poetry builds sdists through "pip --isolated", which ignores PIP_CONSTRAINT
along with every other pip setting, so the previous pin never reached the
environment that compiles pylibfdt and its wrapper still came out with the
PyInt_AsLong call swig 4.5 no longer maps. Install swig below 4.5 into the
project venv, build pylibfdt there without build isolation so it uses that
swig, and let poetry install find the package already satisfied.

Signed-off-by: Cong Wang <cwang@multikernel.io>
A kerf init request is the desired state of the pool, so an option that
names no resource is already the way to say "none of this": --cpus=none,
--memory=none (or --memory=0) and --devices=none each ask for none of
that resource, and a request that asks for nothing at all returns the
whole pool to the host. The --teardown flag only duplicated that one
request behind a second spelling, along with a build_teardown_tree() that
built the same tree the ordinary path builds once the available lists are
empty, so both are gone and a full teardown is now written
"kerf init --cpus=none --memory=none". Mixing none with real entries is
rejected, --cpus and --memory stay required so a forgotten --memory
cannot silently release the pool, and the instance guard, the empty-pool
message, the shortfall note and the dry-run plan all keep working off the
same "is the request empty" test as before. While here, drop the
meaningless "Host Reserved: 0 cpus: []" line kerf show printed for a tree
read back from the kernel, which describes the pool rather than the host.

Signed-off-by: Cong Wang <cwang@multikernel.io>
@congwang-mk
congwang-mk merged commit 0e881bb into main Aug 22, 2026
4 checks passed
@congwang-mk
congwang-mk deleted the pool-reinit branch August 22, 2026 22:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

kerf revert initialize

1 participant