Skip to content

microvm: auto-select chv restore mode / prepare for chv v53 - #847

Open
Benjamin Elder (BenTheElder) wants to merge 3 commits into
agent-substrate:mainfrom
BenTheElder:microvm-ch-restore-mode
Open

microvm: auto-select chv restore mode / prepare for chv v53#847
Benjamin Elder (BenTheElder) wants to merge 3 commits into
agent-substrate:mainfrom
BenTheElder:microvm-ch-restore-mode

Conversation

@BenTheElder

@BenTheElder Benjamin Elder (BenTheElder) commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

What

Make the micro-VM runtime choose how it restores guest RAM based on the
cloud-hypervisor it is actually driving, so we can support v53.

Why

cloud-hypervisor 53.0.0 changed on-demand restore in two ways: the userfaultfd
handler now background-prefaults every registered page (#8150), and a snapshot is
refused while that runs (#8556). Together these make memory_restore_mode=OnDemand
— what we use today — unusable on v53: the prefault storm starves the guest and
its readiness probe never passes. The actor never comes up.

cloud-hypervisor ships minor releases and rarely backports fixes to a patch branch,
so pinning v52 means running an unmaintained hypervisor indefinitely. We need to be
able to keep up.

Restoring eagerly sidesteps both problems: it reads only the snapshot's populated
extents and registers no userfaultfd, so nothing prefaults and nothing gates a later
snapshot. But eager is the wrong choice on v52, where on-demand costs a tenth of the
memory. So this is a per-VMM decision, not a configuration knob.

How

  • vmm.ping already answers before every restore and its reply carries the version;
    we were discarding it. Ping/WaitReady now return it — no extra process, no
    extra round trip, nothing to cache.
  • Pick the mode from that version. The affected range is bounded
    (prefaultingSince/prefaultingUntil) rather than "53 and up forever", so when a
    release stops prefaulting unconditionally, on-demand's smaller footprint comes back
    by moving one constant.
  • An unreadable version restores eagerly and logs it. The two ways to be wrong are not
    equal: guessing on-demand on an affected version leaves the actor unable to start,
    while guessing eager only costs memory.
  • Eager makes the snapshot self-contained, so two things that follow from it: skip the
    merge (it would copy the whole resident set onto the restore source for nothing), and
    drop the staged memory image (nothing pages from it or merges against it afterwards).

Measurements

kind, arm64, 2 GiB guest, counter demo. Each config measured identically, no flags set.

idle memfd VMM RSS pause (median) per-actor disk
v52, before and after this PR 16 MiB 20–21 MiB ~0.26 s 158 MiB
v53, before this PR actor never becomes ready
v53, with this PR 158 MiB 162 MiB ~0.32 s 158 MiB

On v52 the runtime detects 52.0.0, selects on-demand, and reproduces the current
shipped behaviour exactly — this is a no-op on what we run today. On v53 it detects
53.0.0, selects eager, and the actor works.

Within v53, the two follow-on changes are worth their own line:

v53 pause (median) v53 per-actor disk
mode selection only ~0.69 s 318 MiB
+ skip the merge ~0.45 s 318 MiB
+ drop the staged image ~0.32 s 158 MiB

Testing

  • Unit tests for version parsing and selection, including the real vmm.ping payloads
    from both binaries we ship, and a test documenting how to retire the workaround.
  • e2e on kind against both binaries with no flags set: v52 → on-demand, v53 → eager,
    counter continuity across five pause/resume cycles each.
  • go test -race ./cmd/ateom-microvm/... green; builds for linux and darwin.

NOTE: This PR does not upgrade chv to v53+ by default, I'd like to close the remaining gaps first.

vmm.ping already answers before every boot and restore, and its reply carries
the VMM's version; we were discarding it. Return it from Ping and WaitReady so
callers can tell which cloud-hypervisor they are driving without running the
binary again to ask.

Add the classification that goes with it: cloud-hypervisor 53.0.0 made the
userfaultfd restore handler background-prefault every registered page and
refuse a snapshot until that finishes. The affected range is bounded rather
than open-ended, so when a release stops doing it the smaller footprint of an
on-demand restore comes back by moving one constant.
…hat cannot help

An on-demand restore is what we want: pages fault in as the guest touches
them, so an idle restored actor holds its working set rather than its whole
snapshot — 16MiB against 158MiB on the counter demo. On a cloud-hypervisor
that prefaults it is not merely wasteful but unusable, because the prefault
starves the guest and its readiness probe never passes.

Choose per VMM from the version vmm.ping reports. This is a property of the
hypervisor we are driving, not something a deployment should set, so there is
no knob: an unreadable version restores eagerly, which costs memory but always
works, and says so.

Restoring eagerly also reads every populated extent up front, so what the
next snapshot captures is the whole guest and not a delta. Merging that onto
the restore source copies the entire resident set to no purpose, so skip it.
An eager restore reads the whole snapshot into guest memory up front, and
nothing merges against it afterwards, so the staged copy atelet left behind is
dead weight: a second ~160MiB per running actor on top of the checkpoint it
will go on to write. Measured on the counter demo, an actor's on-disk state
halves, 318MiB to 158MiB.

Remove the memory image but keep the directory and the small files beside it.
atelet clears and re-stages the whole directory before any later restore, so
nothing downstream depends on the image still being there.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant