[GH-ISSUE #119] Add opt-in dirty-page tracking for memory snapshot page selection #38

Closed
opened 2026-10-29 14:18:40 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @xeonliu on GitHub (Aug 4, 2026).
Original GitHub issue: https://github.com/kvcache-ai/AgentENV/issues/119

Affected component

Firecracker runtime

Problem statement

AgentENV currently configures fresh VMs with track_dirty_pages = false, and file-backed snapshot restore does not set SnapshotLoadParams.track_dirty_pages. Memory snapshots are requested as Firecracker Diff snapshots, but without dirty tracking Firecracker uses mincore(2) to select pages.

That makes the result sparse, but not truly incremental:

  • Anonymous guest memory includes all currently resident/faulted-in pages, including pages that were only read.
  • A restored VM includes pages from the backing memory image that have merely been faulted in.
  • Firecracker documents swap as a correctness limitation: swapped-out guest pages are not reported by mincore even though a snapshot may need them.

Consequently, a sandbox that changes only a small amount of memory between checkpoints may still write an OverlayBD layer approaching its resident working-set size. Frequent pause, snapshot, resume, and fork operations then pay unnecessary pause latency, storage I/O, object-storage/network transfer, and layer-compaction cost.

Firecracker PR #5274 explicitly distinguishes mincore snapshots from dirty-page tracked snapshots, and Firecracker issue #5768 describes the former as closer to sparse snapshots than differential snapshots:

I did not find an existing AgentENV issue or PR proposing true dirty-page page selection. AgentENV PR #92 adds optional compression, but compression reduces stored bytes rather than selecting only pages written during the current snapshot epoch, and it does not reduce the page set read from Firecracker.

Use case

Long-running agent and RL sandboxes can have 1-8 GiB of guest memory and a large read-mostly working set. Between two checkpoints, the workload may modify only tens of MiB. The same environment may be checkpointed or forked many times during one trajectory.

For these workloads, each memory layer should contain approximately the pages actually written since tracking began, rather than the full resident working set. Reading a large restored working set should not by itself make the next memory layer large.

Current behavior and workarounds

The default direct path is:

pause VM
  -> create state-only Diff snapshot
  -> GET /vm/dirty-memory-ranges
  -> process_vm_readv
  -> compact_to
  -> overlaybd.commit

The legacy path creates a sparse mem.bin Diff snapshot and converts its data extents into overlaybd.commit.

Current mitigations are insufficient:

  • mincore avoids copying never-resident anonymous pages, but cannot distinguish read-only resident pages from written pages.
  • Compression (AgentENV PR #92) reduces artifact bytes but does not reduce the page set read from Firecracker, and it consumes additional CPU during pause.
  • OverlayBD layer compaction controls some chain-growth cost but does not reduce the amount captured at each checkpoint.
  • The native mem.bin path can reuse Firecracker's snapshot transaction, but adds an intermediate large-file write/read cycle before OverlayBD packaging.

Related AgentENV work:

  • #92 compresses memory snapshot layers.
  • #11 discusses transporting incremental memory-layer ancestry for VM migration, without changing page tracking.

Desired behavior

  • Add an explicit opt-in memory-snapshot tracking mode (for example mincore vs dirty) rather than silently changing the current default.
  • In dirty mode, fresh VMs are configured with track_dirty_pages = true and file-backed restores are configured with SnapshotLoadParams.track_dirty_pages = true, so the selected page set comes from the KVM and Firecracker/VMM dirty bitmaps instead of mincore.
  • Read-only resident pages are excluded from the captured set; only pages actually written are selected.
  • Repeated captures of the same running sandbox remain complete relative to the launch-time parent snapshot: the dirty set may accumulate across captures, and this issue does not advance or reset the snapshot epoch (that is a separate issue).
  • The runtime overhead of dirty-page logging is measured and documented before any default change is considered.

Acceptance criteria:

  • Given a fresh VM with dirty mode enabled, when I write only a small set of pages and take a snapshot, then the memory layer contains those written pages and not the full resident set.
  • Given a VM restored from a snapshot with dirty mode enabled, when I read a large portion of guest memory without writing it and take a snapshot, then the captured layer does not materially grow compared with mincore mode.
  • Given two consecutive captures of the same running VM in dirty mode, when the snapshot data is inspected, then neither capture omits pages required for a complete restore relative to the launch parent (cumulative dirty set).
  • A benchmark reports pause latency, selected page count, layer bytes, restore behavior, and steady-state runtime overhead for both mincore and dirty modes.

Proposed approach

Start with an explicit opt-in mode rather than silently changing the current default. The exact config shape is open for discussion, for example:

[memory_snapshot]
tracking_mode = "mincore" # or "dirty"

The dirty mode would likely require:

  1. Set MachineConfiguration.track_dirty_pages = true for fresh VMs.
  2. Set SnapshotLoadParams.track_dirty_pages = true for file-backed restores.
  3. Combine KVM and Firecracker/VMM dirty bitmaps, convert them to the existing destination SegmentMapping representation, and retain the current direct process_vm_readv -> compact_to -> overlaybd.commit data path.
  4. Keep the running instance's dirty set cumulative relative to the original launch parent; do not reset or advance the dirty epoch in this issue so that repeated captures stay correct even before the epoch-advancement work lands.
  5. Add comparative benchmarks for both modes before any default change.

Open questions for maintainers:

  1. Is mincore deliberately preferred to avoid KVM dirty-logging overhead, or is it currently a compatibility/default choice?
  2. In the custom Firecracker fork, does GET /vm/dirty-memory-ranges honor track_dirty_pages (KVM dirty log) and fall back to mincore when it is off?
  3. Is an opt-in dirty mode with benchmarks an acceptable first PR before considering a default change?
  4. Does AgentENV guarantee swap is disabled on runtime nodes, or should startup reject/warn when mincore mode is used with swap enabled?

Compatibility and operational impact

API/config changes:

  • Likely one memory-snapshot tracking-mode setting.
  • No AgentENV public HTTP API change is expected.

Snapshot or storage format changes:

  • None expected. Memory remains a bottom-up list of immutable OverlayBD layers.

New host/runtime requirements:

  • KVM dirty logging must be supported for dirty mode.
  • Runtime CPU/memory overhead of dirty-page logging must be benchmarked before changing the default.
  • The mincore mode's swap precondition should be explicit.

Upgrade and rollback considerations:

  • Start as opt-in so operators can return to mincore mode.
  • Existing committed snapshots remain compatible.

Alternatives considered

  • Keep mincore and rely on compression (AgentENV PR #92): simple, but captures a read-resident superset and does not provide true write-set deltas; compression adds pause CPU.
  • Always enable dirty tracking with no config: simplest, but imposes dirty-logging runtime overhead and portability constraints on all users; a toggle is more appropriate.
  • Compare resident pages with the parent: avoids dirty-logging overhead but adds a large read/compare pass and complicates remote parent layers.
  • Track writes using userfaultfd or page protection: substantially more complexity while Firecracker already supports KVM plus VMM dirty tracking.

Non-goals

  • Advancing the snapshot parent/epoch chain after each capture (separate issue).
  • Changing the OverlayBD on-disk format.
  • Changing rootfs or attached-drive snapshot behavior.
  • Replacing compression or background download.
  • Changing the default mode before benchmarks land.

Contribution

I can implement it

Pre-submission checklist

  • I searched existing issues and discussions and did not find a duplicate.
  • I described a concrete problem and use case, not only a proposed technology.
  • I understand that acceptance of the problem does not imply approval of a particular implementation.
Originally created by @xeonliu on GitHub (Aug 4, 2026). Original GitHub issue: https://github.com/kvcache-ai/AgentENV/issues/119 ### Affected component Firecracker runtime ### Problem statement AgentENV currently configures fresh VMs with `track_dirty_pages = false`, and file-backed snapshot restore does not set `SnapshotLoadParams.track_dirty_pages`. Memory snapshots are requested as Firecracker `Diff` snapshots, but without dirty tracking Firecracker uses `mincore(2)` to select pages. That makes the result sparse, but not truly incremental: - Anonymous guest memory includes all currently resident/faulted-in pages, including pages that were only read. - A restored VM includes pages from the backing memory image that have merely been faulted in. - Firecracker documents swap as a correctness limitation: swapped-out guest pages are not reported by `mincore` even though a snapshot may need them. Consequently, a sandbox that changes only a small amount of memory between checkpoints may still write an OverlayBD layer approaching its resident working-set size. Frequent pause, snapshot, resume, and fork operations then pay unnecessary pause latency, storage I/O, object-storage/network transfer, and layer-compaction cost. Firecracker PR #5274 explicitly distinguishes `mincore` snapshots from dirty-page tracked snapshots, and Firecracker issue #5768 describes the former as closer to sparse snapshots than differential snapshots: - https://github.com/firecracker-microvm/firecracker/pull/5274 - https://github.com/firecracker-microvm/firecracker/issues/5768 I did not find an existing AgentENV issue or PR proposing true dirty-page page selection. AgentENV PR #92 adds optional compression, but compression reduces stored bytes rather than selecting only pages written during the current snapshot epoch, and it does not reduce the page set read from Firecracker. ### Use case Long-running agent and RL sandboxes can have 1-8 GiB of guest memory and a large read-mostly working set. Between two checkpoints, the workload may modify only tens of MiB. The same environment may be checkpointed or forked many times during one trajectory. For these workloads, each memory layer should contain approximately the pages actually written since tracking began, rather than the full resident working set. Reading a large restored working set should not by itself make the next memory layer large. ### Current behavior and workarounds The default direct path is: ```text pause VM -> create state-only Diff snapshot -> GET /vm/dirty-memory-ranges -> process_vm_readv -> compact_to -> overlaybd.commit ``` The legacy path creates a sparse `mem.bin` Diff snapshot and converts its data extents into `overlaybd.commit`. Current mitigations are insufficient: - `mincore` avoids copying never-resident anonymous pages, but cannot distinguish read-only resident pages from written pages. - Compression (AgentENV PR #92) reduces artifact bytes but does not reduce the page set read from Firecracker, and it consumes additional CPU during pause. - OverlayBD layer compaction controls some chain-growth cost but does not reduce the amount captured at each checkpoint. - The native `mem.bin` path can reuse Firecracker's snapshot transaction, but adds an intermediate large-file write/read cycle before OverlayBD packaging. Related AgentENV work: - #92 compresses memory snapshot layers. - #11 discusses transporting incremental memory-layer ancestry for VM migration, without changing page tracking. ### Desired behavior - Add an explicit opt-in memory-snapshot tracking mode (for example `mincore` vs `dirty`) rather than silently changing the current default. - In dirty mode, fresh VMs are configured with `track_dirty_pages = true` and file-backed restores are configured with `SnapshotLoadParams.track_dirty_pages = true`, so the selected page set comes from the KVM and Firecracker/VMM dirty bitmaps instead of `mincore`. - Read-only resident pages are excluded from the captured set; only pages actually written are selected. - Repeated captures of the same running sandbox remain complete relative to the launch-time parent snapshot: the dirty set may accumulate across captures, and this issue does not advance or reset the snapshot epoch (that is a separate issue). - The runtime overhead of dirty-page logging is measured and documented before any default change is considered. Acceptance criteria: - Given a fresh VM with dirty mode enabled, when I write only a small set of pages and take a snapshot, then the memory layer contains those written pages and not the full resident set. - Given a VM restored from a snapshot with dirty mode enabled, when I read a large portion of guest memory without writing it and take a snapshot, then the captured layer does not materially grow compared with `mincore` mode. - Given two consecutive captures of the same running VM in dirty mode, when the snapshot data is inspected, then neither capture omits pages required for a complete restore relative to the launch parent (cumulative dirty set). - A benchmark reports pause latency, selected page count, layer bytes, restore behavior, and steady-state runtime overhead for both `mincore` and dirty modes. ### Proposed approach Start with an explicit opt-in mode rather than silently changing the current default. The exact config shape is open for discussion, for example: ```toml [memory_snapshot] tracking_mode = "mincore" # or "dirty" ``` The dirty mode would likely require: 1. Set `MachineConfiguration.track_dirty_pages = true` for fresh VMs. 2. Set `SnapshotLoadParams.track_dirty_pages = true` for file-backed restores. 3. Combine KVM and Firecracker/VMM dirty bitmaps, convert them to the existing destination `SegmentMapping` representation, and retain the current direct `process_vm_readv -> compact_to -> overlaybd.commit` data path. 4. Keep the running instance's dirty set cumulative relative to the original launch parent; do not reset or advance the dirty epoch in this issue so that repeated captures stay correct even before the epoch-advancement work lands. 5. Add comparative benchmarks for both modes before any default change. Open questions for maintainers: 1. Is `mincore` deliberately preferred to avoid KVM dirty-logging overhead, or is it currently a compatibility/default choice? 2. In the custom Firecracker fork, does `GET /vm/dirty-memory-ranges` honor `track_dirty_pages` (KVM dirty log) and fall back to `mincore` when it is off? 3. Is an opt-in dirty mode with benchmarks an acceptable first PR before considering a default change? 4. Does AgentENV guarantee swap is disabled on runtime nodes, or should startup reject/warn when `mincore` mode is used with swap enabled? ### Compatibility and operational impact API/config changes: - Likely one memory-snapshot tracking-mode setting. - No AgentENV public HTTP API change is expected. Snapshot or storage format changes: - None expected. Memory remains a bottom-up list of immutable OverlayBD layers. New host/runtime requirements: - KVM dirty logging must be supported for dirty mode. - Runtime CPU/memory overhead of dirty-page logging must be benchmarked before changing the default. - The `mincore` mode's swap precondition should be explicit. Upgrade and rollback considerations: - Start as opt-in so operators can return to `mincore` mode. - Existing committed snapshots remain compatible. ### Alternatives considered - **Keep `mincore` and rely on compression (AgentENV PR #92):** simple, but captures a read-resident superset and does not provide true write-set deltas; compression adds pause CPU. - **Always enable dirty tracking with no config:** simplest, but imposes dirty-logging runtime overhead and portability constraints on all users; a toggle is more appropriate. - **Compare resident pages with the parent:** avoids dirty-logging overhead but adds a large read/compare pass and complicates remote parent layers. - **Track writes using userfaultfd or page protection:** substantially more complexity while Firecracker already supports KVM plus VMM dirty tracking. ### Non-goals - Advancing the snapshot parent/epoch chain after each capture (separate issue). - Changing the OverlayBD on-disk format. - Changing rootfs or attached-drive snapshot behavior. - Replacing compression or background download. - Changing the default mode before benchmarks land. ### Contribution I can implement it ### Pre-submission checklist - [x] I searched existing issues and discussions and did not find a duplicate. - [x] I described a concrete problem and use case, not only a proposed technology. - [x] I understand that acceptance of the problem does not imply approval of a particular implementation.
bsakaguchi 2026-10-29 14:18:40 +00:00
Author
Owner

@yingdi-shan commented on GitHub (Aug 4, 2026):

Thanks for raising this. One additional factor worth considering is that AgentENV already enables DAMON reclaim in the guest together with balloon free-page reporting. For filesystem data, there are effectively two cache levels: the underlying image data may already reside in the host page cache, while the guest keeps another copy in its own page cache. Using DAMON allows cold, reclaimable pages to be released from the Firecracker process, so they should no longer be included by mincore.

The current DAMON settings are relatively conservative, and tuning parameters such as min_age, quota_sz and the watermarks more aggressively may reduce snapshot size further, potentially addressing much of this concern without the runtime overhead of dirty-page tracking.

<!-- gh-comment-id:5176089822 --> @yingdi-shan commented on GitHub (Aug 4, 2026): Thanks for raising this. One additional factor worth considering is that AgentENV already enables DAMON reclaim in the guest together with balloon free-page reporting. For filesystem data, there are effectively two cache levels: the underlying image data may already reside in the host page cache, while the guest keeps another copy in its own page cache. Using DAMON allows cold, reclaimable pages to be released from the Firecracker process, so they should no longer be included by mincore. The current DAMON settings are relatively conservative, and tuning parameters such as min_age, quota_sz and the watermarks more aggressively may reduce snapshot size further, potentially addressing much of this concern without the runtime overhead of dirty-page tracking.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
kvcache-ai/AgentENV#38
No description provided.