Repository navigation
Conversation
There was a problem hiding this comment.
🔵 Needs a closer look
The pagemap-driven reset policy affects sandbox isolation and needs fixes plus human validation.
3 open findings
What changed in this PR
This experiment targets faster KVM snapshot restores by retaining and zeroing selected scratch pages in Hyperlight’s host memory manager.
Changes:
- Adaptive pagemap-based scratch resets with platform fallbacks.
- Per-sandbox reset state, memory-behavior tests, and changelog documentation.
| File | Description |
|---|---|
| src/hyperlight_host/src/mem/shared_mem.rs | Selects adaptive KVM resets with zeroing fallbacks. |
| src/hyperlight_host/src/mem/scratch_reset.rs | Implements residency tracking, reset policies, and tests. |
| src/hyperlight_host/src/mem/mod.rs | Provides platform-gated reset modules. |
| src/hyperlight_host/src/mem/mgr.rs | Maintains reset state across scratch restores. |
| CHANGELOG.md | Documents retained KVM scratch memory. |
🧠 Review effort: Balanced
Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.
74aa786 to
b907442
Compare
This comment has been minimized.
This comment has been minimized.
d278d83 to
592e155
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
7a2bee3 to
cb8bbaf
Compare
This comment has been minimized.
This comment has been minimized.
e4d8f03 to
e53c815
Compare
Patch hyperlight-host and hyperlight-common to danbugs/hyperlight c854bb43 (0.17.0 plus the scratch reset of hyperlight-dev/hyperlight#1901), to measure it in CI. Signed-off-by: danbugs <danilochiarlone@gmail.com>
This comment has been minimized.
This comment has been minimized.
A restore zeroed scratch by dropping it with MADV_DONTNEED on KVM (fill(0) in builds with mshv3), by fill(0) on MSHV, and by mapping fresh memory on Windows (#1765). Each costs time in the size of scratch, or a fault on every page the next run touches. On KVM, every reset now reads /proc/self/pagemap for all of scratch. A page with memory of its own that holds data is zeroed and kept, so the next run takes no faults on it. One that holds only zeros was not written since the last reset and is dropped, so what stays resident follows what runs write. Swapped pages and shared pages holding data are dropped. The rule is the same from the first restore on. Scratch is backed 4 KiB at a time on KVM, so a reset zeroes only what was touched. On Windows, scratch of up to 16 MiB is now zeroed in place, which is about 2x faster than a fresh mapping (over 5x when the guest writes a MiB or more); larger scratch is still replaced, so it does not all become resident. HostSharedMemory now notes the pages its writes touch, and zero_written zeroes only a given set of pages plus those, for resets that know what the guest wrote (next commits). Signed-off-by: danbugs <danilochiarlone@gmail.com>
Add a DirtyLog supertrait of VirtualMachine: how a VM tracks the pages the guest writes, and reading and clearing that log for a range. On MSHV (x86_64), tracking is a partition property switched on and off. Reads use get_dirty_log with clear; stopping sets the bits back first, which MSHV requires. On WHP (x86_64 and ARM64), scratch is mapped with WHvMapGpaRangeFlagTrackDirtyPages and read with WHvQueryGpaRangeDirtyBitmap, which clears what it reads. If the tracked mapping fails, scratch is mapped as before and tracking is not tried again for the VM. Other backends do not track. A backend that claims to but does not read reports every page, so a reset zeroes too much, never too little. Signed-off-by: danbugs <danilochiarlone@gmail.com>
Where the hypervisor logs the guest's writes, a restore now zeroes only the pages written since the last restore: those in the log, the pages host writes touched, and the page-table copy (#1766). Tracking starts when scratch is mapped, before the guest runs, so the first restore uses it too (hyperlight_vm/dirty_log.rs): - WHP: always. Tracking costs the guest nothing measurable there. The log is cleared when scratch is mapped, so only written pages are ever touched and the rest of scratch never becomes resident. - MSHV: tracking makes the guest's first write to each page after a read fault to the hypervisor (about 1 us nested), and reading the log costs time in the size of scratch. Measured against zeroing all of scratch, that loses below 2 MiB of scratch, and once a run writes more than about a tenth of scratch (a quarter from 32 MiB, where zeroing gets about 3x slower per MiB). So scratch under 2 MiB is not tracked, and tracking stops after such a run until scratch is mapped again. The first log after mapping reports every page, so the guest sets up without faults. - KVM keeps its in-place reset; no log is read. Signed-off-by: danbugs <danilochiarlone@gmail.com>
The restore benchmarks use 352 KiB to 1 MiB of scratch, where resetting it costs little however it is done. Add call_with_restore with 64 and 256 MiB of scratch, with a guest that writes a few pages (Echo) and one that writes 1 MiB a call, and report them, with the default size, in pull request comments. Signed-off-by: danbugs <danilochiarlone@gmail.com>
e53c815 to
cce31c1
Compare
This comment has been minimized.
This comment has been minimized.
|
Hi this looks good! I reviewed this with help of ai. A couple of points:
Everythign under here is ai generated, so please take it with a grain of salt. I think most the points are valid though: -- slop line -- KVM
MSHV
WHP
All platforms
Nits
MeasurementsThe call after a restore got slower in these cases, measured against base
The restore itself got 2x to 44x faster in the 256 MiB cases, which is why call plus restore still improves everywhere else. Each iteration ran one call and then a restore, timed separately, on nested Azure VMs with 4 vCPUs, default features, and release builds. |
perf(mem): reset scratch in place and log host writes to it A restore zeroed scratch by dropping it with MADV_DONTNEED on KVM (fill(0) in builds with mshv3), by fill(0) on MSHV, and by mapping fresh memory on Windows (#1765). Each costs time in the size of scratch, or a fault on every page the next run touches. On KVM, every reset now reads /proc/self/pagemap for scratch, as ranges of pages in use with PAGEMAP_SCAN on Linux 6.7 or newer. A page with memory of its own that holds data is zeroed and kept, so the next run takes no faults on it. One that holds only zeros is kept as it is: a guest may write zeros to it every run, and dropping it would make every run fault on it again. Every 16th reset gives those back instead, so what stays resident follows what recent runs wrote. Swapped pages and shared pages holding data are dropped. Scratch is backed 4 KiB at a time on KVM, so a reset zeroes only what was touched. Pagemap is opened once per process. On Windows, scratch of up to 16 MiB is now zeroed in place, which is about 2x faster than a fresh mapping (over 5x when the guest writes a MiB or more); larger scratch is still replaced, so it does not all become resident. HostSharedMemory now notes the pages its writes touch, and writes with exclusive access count as touching all of it. zero_written zeroes only a given set of pages plus those, for resets that know what the guest wrote (next commits). Signed-off-by: danbugs <danilochiarlone@gmail.com>
Signed-off-by: danbugs <danilochiarlone@gmail.com>
perf(sandbox): reset scratch from the dirty log on MSHV and WHP Where the hypervisor logs the guest's writes, a restore now zeroes only the pages written since the last restore: those in the log and the pages host writes touched, the page-table copy included (#1766). Tracking starts when scratch is mapped, before the guest runs, so the first restore uses it too (hyperlight_vm/dirty_log.rs): - WHP: always. Tracking costs the guest nothing measurable there. A range mapped with tracking starts clean, so only written pages are ever touched and the rest of scratch never becomes resident. - MSHV: tracking makes the guest's first write to each page after a read fault to the hypervisor (about 1 us nested), and reading the log costs time in the size of scratch. Measured against zeroing all of scratch, that loses below 2 MiB of scratch, and once a run writes more than about a tenth of scratch (a quarter from 32 MiB, where zeroing gets about 3x slower per MiB). So scratch under 2 MiB is not tracked, and after such a run tracking pauses for 16 restores, which zero all of scratch, then resumes. The first log after mapping reports every page, so the guest sets up without faults. - KVM keeps its in-place reset; no log is read. Signed-off-by: danbugs <danilochiarlone@gmail.com>
bench: restore with large scratch The restore benchmarks use 352 KiB to 1 MiB of scratch, where resetting it costs little however it is done. Add call_with_restore with 64 and 256 MiB of scratch, with a guest that writes a few pages (Echo), 1 MiB or 16 MiB a call, and 32 light calls after one that writes 32 MiB. Time the call after a restore and the restore after a call apart too, since a restore can leave the next call faults to take. Report them, with the default size, in pull request comments. Signed-off-by: danbugs <danilochiarlone@gmail.com>
This comment has been minimized.
This comment has been minimized.
Signed-off-by: danbugs <danilochiarlone@gmail.com>
- KVM keeps scratch pages holding only zeros, and every 16th restore gives them back (64 runs at most). Dropping them on every restore made a guest that writes zeros fault them back in on each call. - MSHV pauses dirty tracking for 16 restores after a run writes much of scratch, instead of stopping it for good. - One pagemap fd per process; a forked child opens its own. pagemap_scan fills its buffer from a borrowed slice. - Host writes through exclusive access note all of scratch, and the page-table copy goes through the logged write. - map_gpa_range is unsafe, with its safety contract documented. Signed-off-by: danbugs <danilochiarlone@gmail.com>
…ight [do not merge] build against the resident scratch reset hyperlight Patch hyperlight-host and hyperlight-common to danbugs/hyperlight ff80dda2 (0.17.0 plus the scratch reset of hyperlight-dev/hyperlight#1901), to measure it in CI. Signed-off-by: danbugs <danilochiarlone@gmail.com>
|
Thanks for the review! I pushed fixups for most of it, to be squashed before merge. Benchmarks
KVM
MSHV
WHP
All platforms
Nits
|
Benchmark ResultsMeasured commit: kvm / amd (Linux) (➖ stable)No benchmark improved or regressed. Benchmark Resultsfunction_call_codec
guest_calls
payload_allocation
sandboxes
slot_pool
snapshot_files
snapshots
virtq_readonly
virtq_readwrite
kvm / intel (Linux) (➖ stable)No benchmark improved or regressed. Benchmark Resultsfunction_call_codec
guest_calls
payload_allocation
sandboxes
slot_pool
snapshot_files
snapshots
virtq_readonly
virtq_readwrite
mshv3 / amd (Linux) (➖ stable)No benchmark improved or regressed. Benchmark Resultsfunction_call_codec
guest_calls
payload_allocation
sandboxes
slot_pool
snapshot_files
snapshots
virtq_readonly
virtq_readwrite
mshv3 / intel (Linux) (❌ 1.77x)Top regressions
Benchmark Resultsfunction_call_codec
guest_calls
payload_allocation
sandboxes
slot_pool
snapshot_files
snapshots
virtq_readonly
virtq_readwrite
hyperv-ws2025 / amd (Windows) (🌟 5.75x)Top improvements
Benchmark Resultsfunction_call_codec
guest_calls
payload_allocation
sandboxes
slot_pool
snapshot_files
snapshots
virtq_readonly
virtq_readwrite
hyperv-ws2025 / intel (Windows) (🌟 6.07x)Top improvements
Benchmark Resultsfunction_call_codec
guest_calls
payload_allocation
sandboxes
slot_pool
snapshot_files
snapshots
virtq_readonly
virtq_readwrite
Reported by |
|
I haven't reviewed this code yet (would like to do so later) but I had a couple of comments based on the PR description. I agree with @ludfjig that the performance tradeoff with actual guest execution is significant and not well accounted for here. I recall that we did some benchmarking on a similar mechanism for tracking dirty pages long ago, before guest cow, and it was not a win in most cases. Since the performance impact is pretty opaque, I think it's important that if this is tuned transparently with heuristic thresholds we should require them to be calibrated for the actual deployment target---they will vary a lot per machine, HV/driver version (I think various people are testing/deploying on at least 3 versions of the mshv driver which have drastically different performance here---we should also detect when movable pages are on and do the pagemap thing in that case), etc. so setting static thresholds in the code is a good recipe for them to get out of date. I'm imagining here something like encouraging app authors who have a wide range of sandboxes to do hyperlight_host::calibrate_memory_restore_heuristics() once early on a machine to get a little serialisable blob that they can pass in to sandbox creation---something that makes it clear that just setting static thresholds is unlikely to last well). (I also think it is probably worth having an override for the heuristics, even if the heuristics are initialised based on benchmarking on the machine). I also think that a big part of the motivation here is just the problem of having the scratch region be oversized drastically. One other thing that might be worth looking into instead is whether things like "allow changing the size of the scratch region after a snapshot" would allow workloads to tune their scratch sizes to be closer to the actual amount of data written, largely eliminating the gap. It might also be worth eventually revisiting the question of whether allowing sandboxes to request more memory from the host is worthwhile, since it would make sizing these things neatly easier. Shrinking the scratch region should have a better performance impact overall without the tradeoff in guest execution time. |


Fixes #1766.
A restore must leave scratch as it was at the snapshot: all zero. Today each hypervisor does that by discarding everything:
MADV_DONTNEED, or zeroes all of it withfill(0)in builds with MSHV support.So a restore costs time in the size of scratch, or a fault on every page the next run touches, however little the guest wrote. This PR makes a restore reset only what changed:
/proc/self/pagemap.PAGEMAP_SCAN, which skips untouched regions, so a restore costs time in how much scratch is in use, not in its size. Older kernels read one entry per page (about 500 µs for 256 MiB).HostSharedMemoryrecords every page its writes touch, and resets zero those too. A write through exclusive access counts as touching all of scratch.Every threshold comes from measurements. I measured scratch from 1 to 256 MiB, with guests writing from nothing up to a third of it:
pagemap.On MSHV, tracking made write-heavy guests slower, as #683 found, which is why tracking pauses past those fractions.
Larger sandboxes (simpleguest restore + call; KVM and MSHV on nested Azure VMs, Windows on bare metal). The default sandbox is unchanged.
hyperlight-unikraft restore + call, stock 0.17.0 → this PR: