vmm: Pin and parallelize memory prefault - #8785
Conversation
d5fddac to
0d80801
Compare
rbradford
left a comment
There was a problem hiding this comment.
Why are you prefaulting?. Is it because you need huge pages and your configuration predates reserve=on ?
The usecase is a guest with identity-mapped (no viommu) VFIO devices. The boot sequence for that guest: Without prefault the three RAM ioctls serially absorb the ~3 TiB of
|
| handles.push(handle); | ||
| } | ||
|
|
||
| for handle in handles { |
There was a problem hiding this comment.
We don't have to wait here. This join can be deferred until vCPU is about to run first time, and let the main thread go.
There was a problem hiding this comment.
This change requires more code than what it appears.
With current implementation :
t=0 workers start populating
boot thread WAITS at the join ──────── 7.4s
t=7.4 device 0: MAP_DMA of RAM → pages all present → pin only, fast
BARs, devices 1..N, memslots ... ~24s
t≈32 vCPUs run
When we defer the join to happen before the vcpu start phase
t=0 workers start populating; boot thread proceeds immediately
t≈0.3 device 0: **MAP_DMA of all RAM** regions issued on the vmm thread
→ the ioctl must pin EVERY page of the region
→ pinning a page requires the page to exist
→ most pages don't exist yet, the workers are still zeroing
→ so the vmm thread sits INSIDE the ioctl until population
finishes (GUP either faults missing pages itself or blocks
on the fault mutex of whichever worker owns them)
t≈7.4+ MAP_DMA finally returns
BARs, devices 1..N ... ~24s
t≈32+ vCPUs run ← same as today
As you see we wont shave off ~7 seconds by deferring the join
when MAP_DMA of RAM regions will convoy with our prefault workers
by contending the mmap lock. The prefault workers can hold the
read lock for a long time as each thread can fault >= 1 hugepage.
The proper way to shave off the ~7 seconds would require us
to change the current code and defer RAM MAP_DMA after
our join, before the vcpus run.
t=0 workers start populating
boot thread proceeds: region info, BAR GPAs, BAR mmaps,
p2p maps, memslots for all 15 devices ... real work,
genuinely overlapping the 7.4s of zeroing
t=7.4 populate done (join, now placed late — before vCPUs run)
t≈7.4+ RAM MAP_DMA — moved here, pages all present, pin only, fast
t≈25 vCPUs run ← the ~7s win
I also think we would require our prefault workers to fault 1 hugepage
at a time instead of the slice range we have
# current
madvise(slice.addr, slice.len, MADV_POPULATE_WRITE);
# proposed
for chunk in slice.split(1 GiB) {
madvise(chunk.addr, chunk.len, MADV_POPULATE_WRITE);
}
I believe we should do this in a new PR.
Let me know your thoughts.
d8f6d69 to
d01e1e2
Compare
Prefault workers inherit the process affinity, so on multi socket hosts some of them zero guest memory across the interconnect, and memory zones are prefaulted one after another, so a guest with one zone per NUMA node zeroes each node while the other idles. Pin each worker, best effort, to the CPUs of its zone's host NUMA node, drop the fixed cap of 16 workers in favor of the host's available parallelism, and prefault all regions concurrently. Snapshot restore prefaults its regions the same way. The one exception is a region backing a virtio-mem hotplug area, which still prefaults inline when it is created at boot. Under landlock, zones with a host_numa_node add a read rule for /sys/devices/system/node so the cpulist stays readable. One barrier spans all workers across regions, since a thread stack allocation takes the mmap lock as a writer during another region's populate read holds and a queued writer blocks later readers, which would convoy every pool down to a few running threads. The RAM region mapping log line now prints before population and a new line reports once all regions are prefaulted. On a dual socket HGX B300 host, a large guest VM with 240 vCPUs and ~3 TiB of memory, one 1477 GiB zone per NUMA node on 1 GiB hugepages, prefaulted in 51.8s at 57 GiB/s before this change and in 7.4s after, sustaining 400 GiB/s across the two sockets. Assisted-by: Claude Code:claude-fable-5 Signed-off-by: Saravanan D <saravanand@crusoe.ai>
The memory prefault test now gives one zone a host_numa_node while the other zone keeps none, so the pinned and the default unpinned worker paths both run in the same boot. A new snapshot restore variant restores with prefault enabled and asserts the completion log line, covering the deferred restore prefault path. Assisted-by: Claude Code:claude-fable-5 Signed-off-by: Saravanan D <saravanand@crusoe.ai>
d01e1e2 to
e5debc8
Compare
Summary
Memory prefault workers inherit the process affinity, so on multi socket
hosts some of them zero guest memory across the interconnect. Memory zones
are also prefaulted one after another, so a guest with one zone per NUMA
node zeroes each node while the other idles.
This series pins each prefault worker, best effort, to the CPUs of its
zone's
host_numa_node, raises the per region worker cap from 16 to 32,and prefaults all regions concurrently. Snapshot restore prefaults its
regions the same way.
Measurements
On a dual socket HGX B300 host, a guest VM with 240 vCPUs and ~3 TiB of
memory, one 1477 GiB zone per NUMA node on 1 GiB hugepages, prefaulted in
51.8s at 57 GiB/s before this change and in 7.4s after, sustaining
400 GiB/s across the two sockets.
Notes
One barrier spans every region's workers. A thread stack allocation
takes the mmap lock as a writer while each populate call holds it as a
reader for its whole range, so spawning must finish before any populate
starts or the pools convoy down to a few running threads.
Pinning is strictly best effort. A missing sysfs cpulist, a memory only
node, or a cpuset that rejects the affinity call all fall back to the
current unpinned behavior. Pinning failures never fail the boot.
Under landlock, zones with a
host_numa_nodeadd a read rule for/sys/devices/system/nodeso the prefault workers can read the nodecpulist.
The one exception is a region backing a
virtio-memhotplug area,which still prefaults inline when it is created at boot.
The RAM region mapping log line now prints before population and a new
prefaulted N memory regionsline reports once all regions arepopulated.
Tests
Unit tests cover the cpulist to
cpu_set_tconversion and theIntegerListparser.Integration tests.
test_memory_prefaultboots two prefault zones, onepinned via
host_numa_node=0and one unpinned, and asserts the guestmemory is fully resident.
test_snapshot_restore_prefaultrestores withprefault=onand asserts the completion log line.Commits
vmm: Pin and parallelize memory prefaultcarries the change and unittests for the cpulist to
cpu_set_tconversion.tests: Extend memory prefault coveragepins one zone of the memoryprefault test so the pinned and unpinned worker paths both run in CI, and
adds a snapshot restore variant that restores with
prefault=onandasserts the completion log line.