Skip to content

vm: cpuset fence — reserve host cores from the VM population (#184) - #185

Merged
CMGS merged 7 commits into
masterfrom
feat/cpuset-fence
Aug 5, 2026
Merged

vm: cpuset fence — reserve host cores from the VM population (#184)#185
CMGS merged 7 commits into
masterfrom
feat/cpuset-fence

Conversation

@CMGS

@CMGS CMGS commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Implements #184 (frozen after one review round).

What

Machine fence (cgroup_cpus, e.g. "0-14"): one read-gated cpuset.cpus write on the #182 parent slice; every VM scope inherits, so the fleet — including the virtio/io-wq workers CH's vCPU affinity cannot reach — physically cannot occupy reserved cores. Overcommit happens inside the fence; reserved cores stay available to the host, vk, and the clone path (the motivating tail: CH clone p95 24.9s / max 69.2s under load).

Per-VM placement (--cpuset-cpus on run/clone): explicit opt-in pinning, snapshot-knobs-are-records semantics (clone from flags alone, restore keeps the VM's).

Enforcement is cocoon's, not the kernel's

cpuset.cpus is a requested set — the kernel silently degrades ungrantable requests instead of failing. So: the fence validates against the ancestor's effective set, placements against the parent's, and a fence shrink is refused while a live VM's placement conflicts. Comparisons use parsed sets because the kernel echoes cpu lists canonicalized ("0-3,4-7" reads back "0-7") — raw-string comparison would defeat the read-gates and re-issue the globally-serialized cpuset write every launch.

Convergence

Clearing cgroup_cpus resets a stale fence once (read-gated; controller never disabled). Empty config on a fresh host writes nothing — byte-for-byte #182 behavior.

queue_affinity clamp

Disk queue affinity used to pin queues to host CPUs 0..N-1 unconditionally; targets now clamp to the effective set (placement > fence > all) on every path that builds or patches disks: launch args, restore's config patch, clone's config patch + hot-added cidata/data disks, and runtime DiskAttach.

Not in this PR

Hardware acceptance (mpstat core-idle proof, clone-latency A/B under saturation, out-of-fence rejection, rollback convergence) runs as a follow-up round per the issue's list.

CMGS added 4 commits August 5, 2026 15:30
A saturated fleet leaves clone/wake execution, vk, and the OS nothing
to run on: field data shows CH clone p95 24.9s / max 69.2s under load.
cgroup_cpus writes a one-time cpuset fence on the parent so VM threads
(including virtio/io-wq workers that CH's vCPU affinity cannot reach)
physically cannot occupy reserved cores; --cpuset-cpus adds explicit
per-VM placement.

The kernel treats cpuset.cpus as a request and silently degrades
ungrantable sets, so subset validation is cocoon's: fence against the
ancestor effective set, placements against the parent's, and a fence
shrink is refused while a live placement conflicts. A cleared config
resets a stale fence once (read-gated, controller never disabled).
Disk queue_affinity targets clamp to the effective set on every path
that builds or patches disks — launch, restore, clone, DiskAttach.
…veCPUs single source

placeScope enabled +cpuset on the parent alone — with no fence ever set
the ancestor chain never delegated cpuset and a bare --cpuset-cpus
failed; controller enablement now runs the same forEachLevel walk in
ensureParent for fence and placement alike. Fence and placement writes
compare parsed sets, not raw strings: the kernel echoes cpu lists
canonicalized, so a non-canonical config string would re-run the scan
and the serialized cpuset write on every launch. The placement-wins
precedence lives once in cgroup.EffectiveCPUs; clone derives it once
and threads it through resume opts.
…ent trims

One forEachLevel walk enables cpu and cpuset together — half the
subtree_control reads per fenced launch and one combined write (one
cgroup_mutex acquisition) on first setup. patchDisks builds the
affinity slice once per call instead of per disk. Six comments fail
the deletion test: restated godoc, duplicated WHYs, narration.
41 files, net -166 lines; content unchanged, only 80-column wrapping
merged to match the one-physical-line style the newer packages use.
@CMGS

CMGS commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Hardware round — 16-core testbed, binary pr185-01aad9f, regression against the merged #182 behavior

Positive matrix ran cocoon as root with a root-level parent (production shape). One structural note confirmed on the way: non-root + a root-level parent cannot work — clone3's common-ancestor check lands on the root cgroup — so non-root remains delegated-user-slice territory (as in the #183 round), where cpuset is typically not delegated; that path correctly fails preflight instead of degrading.

item result
preflight (no cpuset delegation) fence on the user slice → write /sys/fs/cgroup/user.slice/cgroup.subtree_control: permission denied — exact file named
fence confinement cgroup_cpus=0-14: parent cpuset.cpus/effective = 0-14; every VMM thread Cpus_allowed_list ⊆ 0-14, disk-queue threads clamped to 0/1 inside the fence
out-of-fence placement --cpuset-cpus 15cocoon rejects: cpu 15 not in /sys/fs/cgroup/cocoon-test.slice effective set "0-14" (kernel would have silently degraded)
placement --cpuset-cpus 2-3 on 2-vCPU VM → scope effective 2-3, all threads exactly 2-3, queue affinity clamped inside
fence shrink cgroup_cpus=0-1 while the 2-3 placement lives → refused, naming the conflicting VM, cpu, and the remedy
rollback fence cleared from config → parent cpuset.cpus reset to empty on the next launch, once
#182 regression 3 CPU hogs in a 2-vCPU VM: 19988ms consumed over a 10s window = the 2-core cap exactly (nr_throttled 6→30); stop removes the scope, start rebuilds weight=2 / 200000 100000 / cpuset 0-14 byte-identical

Environment fully torn down (scopes 0, test slice removed, isolated root deleted).

CMGS added 3 commits August 5, 2026 17:11
--cpu is now also the hard cap and every VM enters a scope — the CLI
flag tables, config keys, snapshot-inheritance wording, and a new CPU
Isolation section in vm.md catch the docs up with #182 and #184.
The fence bounds the VMs; the caller's share is set on its own systemd
unit — same cgroup files, plus the reserved-core-as-fast-lane guidance
over pinning.
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.

1 participant