Skip to content

feat: add mincore snapshot pre-fault - #219

Open
Hygge-Gezelligheid wants to merge 12 commits into
kvcache-ai:mainfrom
Hygge-Gezelligheid:agent/mincore-prefault-mvp
Open

feat: add mincore snapshot pre-fault#219
Hygge-Gezelligheid wants to merge 12 commits into
kvcache-ai:mainfrom
Hygge-Gezelligheid:agent/mincore-prefault-mvp

Conversation

@Hygge-Gezelligheid

@Hygge-Gezelligheid Hygge-Gezelligheid commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Scope

This draft implements the public AgentENV mincore snapshot pre-fault path, including correctness repairs required for the production restore path and reproducible benchmark coverage.

  • Base: main@58732505ba415ac691f2eee2fe7b1af39ba7058e
  • Firecracker implementation dependency: kvcache-ai/firecracker#21
  • Shipping dependency: a Firecracker binary published from that PR (the currently available v1.15.1-patch-v2 artifact does not return the completion stats this revision requires).

What changes

  • Persist and carry working-set metadata through committed snapshots, factory/override restores, paused restores, and forks; profile through the envd-ready boundary rather than envd-initialized.
  • Validate and normalize GPA ranges, apply platform/capability gates before guest-memory API use, and issue pre-fault before resume.
  • Require Firecracker completion statistics for a successful benchmark sample: requested bytes must equal completed bytes and remaining bytes must be zero.
  • Regenerate the Firecracker client schema/models, including pre-fault completion statistics and corrected response definitions.
  • Add a benchmark-only explicit --max-prefault-bytes override and a deterministic fixed-512-MiB mechanism benchmark. Product defaults remain unchanged at 256 MiB.
  • Add production-path tests, including committed snapshot/factory startup coverage rather than private-field-only setup.

Validation

Passed on Madsys:

  • cargo test -p agentenv --lib prefault_stats — 3 passed
  • cargo test -p agentenv --lib manifest — 33 passed
  • cargo test -p firecracker_client
  • cargo check -p agentenv-benchmarks --bench snapshot
  • cargo bench -p agentenv-benchmarks --bench snapshot --no-run
  • cargo fmt --check and git diff --check
  • make test-unit PROFILE=debug — AgentENV 806 passed / 4 ignored; linux-cap 3 passed

Firecracker-specific unit, integration, and API-route validation is recorded in firecracker#21.

Performance evidence

The included raw logs and report distinguish an isolated fixed working-set mechanism test from the product path. All fixed-512-MiB samples verified requested == completed == 536,870,912 and remaining == 0.

Product resume → envd-ready

  • Resource-cold (no anchor sandbox; not a claim of physically cold host cache): 20 ABBA samples per arm; mean 142.24 ms without pre-fault vs 123.96 ms with pre-fault, a 12.8% / 18.27 ms improvement.
  • Hot shared resource: 85.22 ms without vs 85.65 ms with pre-fault; effectively neutral (+0.5%).

Fixed 512 MiB multi-vCPU mechanism test

All configurations pre-fault the same deterministic 512 MiB GPA range; only snapshot vCPU/worker count changes. Each row is 10 samples of complete pre-fault wall time, not guest scan or end-to-end resume time.

vCPU / workers Mean Median Relative to 1 worker
1 257.329 ms 257.458 ms 1.00×
2 292.257 ms 294.133 ms 0.88× (slower)
4 234.023 ms 231.807 ms 1.10×
8 195.458 ms 193.150 ms 1.32×

Per-worker monotonic timelines show the 2-worker ioctls overlap by about 99.8–100%, so this is not userspace dispatch/join serialization. In a focused 1→2 diagnostic, wall time rose 231.2 → 276.7 ms (+19%) while completed uBlk read bytes changed only 578.4 → 589.0 MiB (+1.8%); successful reads rose 4,580 → 6,012 (+31%) and mean request size fell about 126 → 98 KiB.

The supported conclusion is therefore limited: workers execute concurrently and completion accounting is correct, but this host does not show near-linear multi-vCPU pre-fault scaling. The 2-worker regression is accompanied by backing-read request fragmentation, placing the remaining question below userspace worker dispatch in the KVM / host page-fault / backing-uBlk path. This PR deliberately makes no multi-vCPU performance claim.

The product command measured here is snapshot load → envd-ready; it is not a webpage/application first-request benchmark.

Review / merge boundary

Do not merge or release this AgentENV PR until the Firecracker dependency PR is accepted and a matching immutable binary asset is published. The project CI jobs for this head have passed; a separate code-review workflow is still in progress.

@Hygge-Gezelligheid
Hygge-Gezelligheid marked this pull request as ready for review August 30, 2026 16:17
@github-actions

github-actions Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

🔍 OpenCodeReview found 25 issue(s) in this PR.

  • ✅ Successfully posted inline: 25 comment(s)

Comment on lines +99 to +103
anyhow::ensure!(
path.is_file(),
"--firecracker-binary does not name a file: {}",
path.display()
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · low
This validates only that the path is a regular file, although the option contract says it requires an executable. A non-executable file passes parsing and fails much later during sandbox startup. On Unix, also validate that at least one execute mode bit is set (or adjust the diagnostic to avoid claiming executable validation).

Comment on lines +405 to +410
let mut sandbox = setup_sandbox_with_vcpu(vcpu_count).await?;
let reported_vcpu_count = guest_vcpu_count(&sandbox).await?;
anyhow::ensure!(
reported_vcpu_count == vcpu_count,
"guest reports {reported_vcpu_count} vCPUs; benchmark requested {vcpu_count}"
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
Either the nproc query or the count assertion can return after startup without calling sandbox.stop(). This leaks the same daemon-managed resources when a requested multi-vCPU configuration is unsupported or misreported. Ensure the sandbox is explicitly stopped on every validation/pause error path, not only on success.

Comment on lines +687 to +694
run_guest_shell(
&sandbox,
PREFAULT_WORKLOAD_SETUP,
"prefault workload fixture setup",
)
.await?;
let snapshot = sandbox.pause().await?;
sandbox.stop().await?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
A fixture setup or pause error bypasses sandbox.stop(), leaving the started sandbox's daemon-managed resources allocated. Run setup/pause into a captured result and stop unconditionally before propagating that result (while preserving both errors if cleanup also fails).

Comment on lines +776 to +783
Some(expected) => {
let reported = guest_vcpu_count(&sandbox).await?;
anyhow::ensure!(
reported == expected,
"restored guest reports {reported} vCPUs; expected {expected}"
);
Ok(())
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
This ? can return before sandbox.stop() runs. FirecrackerSandbox::drop only performs best-effort process/network cleanup and explicitly does not release daemon-managed ublk devices, so a failed nproc call can leave benchmark resources allocated and contaminate later samples. Capture this result like command_result, then always call stop() before propagating it; the hot-holder readiness error path in prefault_measurement_samples needs the same cleanup guarantee.

Comment on lines +812 to +815
let sandbox = FirecrackerSandbox::resume_from_snapshot_config_with_prefault(
snapshot,
prefault_enabled,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test · medium
The hot-path holder uses the arm's pre-fault setting. In the enabled arm it actively faults the profiled ranges and keeps their backing pages resident, while the disabled holder does not, so the two hot arms begin from different cache/residency states beyond the operation being timed. This can attribute holder-induced cache warmth to pre-fault performance. Warm both arms with an identical holder policy (or explicitly normalize residency) before measuring.

Comment on lines +853 to +855
let envd_ready_started = std::time::Instant::now();
self.wait_for_ready().await?;
timings.envd_ready = envd_ready_started.elapsed();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
envd_ready is documented and reported by the benchmark as the envd-readiness stage, but wait_for_ready() also releases background downloads and executes envd_instance.init(). Likewise, snapshot_load currently includes MMDS setup and disk-rate-limiter reconciliation after load_snapshot_file. These measurements therefore do not isolate the advertised boundaries and can misattribute meaningful latency. Time wait_for_envd_ready() directly (then perform initialization separately), and stop the snapshot-load timer immediately after the load request or rename/document the broader stages.

Comment on lines +1194 to +1196
warn_if_mincore_host_has_swap();
let mut profiler = Self::from_profiling_snapshot_config(snapshot)?;
let result = async {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
Cleanup here is not cancellation-safe. stop() runs only after the inner future returns, so dropping/aborting this public profiling future while it waits for envd (or caller workload) drops profiler without asynchronously releasing profiling_mem_ublk_device; UblkDevice is a pure data handle and FirecrackerSandbox::drop does not release it. This affects all profiling methods using this pattern. Use a cancellation-safe async cleanup guard/owned task, or add a drop fallback that schedules release of the exclusive device.

Comment on lines +1443 to +1444
let working_set = resident_ranges_to_working_set(&newly_resident, &regions, limits)?;
debug!(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
The returned working set is mislabeled for this API. resident_ranges_to_working_set constructs it through GuestMemoryWorkingSet::new, which hard-codes observation_window = "snapshot-resume-to-envd-ready", but this method samples after envd initialization and the caller-supplied workload. If this result is published or compared by its metadata, consumers cannot distinguish it from the ready-only profile. Construct the result with a workload-specific observation window (and update validation to recognize that typed value), or keep workload profiles in a separate diagnostic type that cannot be published as ready-window metadata.

Comment on lines +1500 to +1507
if let Some(mem_device) = self.profiling_mem_ublk_device.take() {
if let Err(e) = UblkDeviceManager::global()
.release_device(&mem_device)
.await
{
warn!(error = %e, "failed to release profiler memory ublk device during stop");
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
This cleanup is skipped whenever fc_instance.stop(...).await above returns an error because stop() returns immediately at that ?. Unlike SharedMemDevice, this exclusive profiler handle has no drop cleanup, so a failed Firecracker stop leaks the daemon device even when the profiling future runs its explicit cleanup path. Preserve the Firecracker stop error, still release the profiler device (and other resources), then return the accumulated error.

Comment on lines +1883 to +1885
let stats =
PrefaultCompletionStats::from_api(api_stats, ranges.len(), bytes)
.context("validate Firecracker pre-fault completion stats")?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · high
A validation discrepancy in this optional performance hint aborts snapshot resume even though Firecracker has already accepted/completed the pre-fault request. The same method also propagates transport/read failures while only HTTP statuses are fail-open. When pre-faulting is enabled, transient API errors or a compatible server with differing statistics can therefore make an otherwise restorable VM unavailable. Log these failures and continue without verified stats; reserve a hard error for failures that leave VM state unsafe to resume.

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