Skip to content

feat: support PVM-backed deployment - #87

Open
LSX-s-Software wants to merge 3 commits into
kvcache-ai:mainfrom
LSX-s-Software:feat/pvm
Open

feat: support PVM-backed deployment#87
LSX-s-Software wants to merge 3 commits into
kvcache-ai:mainfrom
LSX-s-Software:feat/pvm

Conversation

@LSX-s-Software

@LSX-s-Software LSX-s-Software commented Jul 30, 2026

Copy link
Copy Markdown
Member

What

Add an opt-in PVM virtualization mode alongside the existing KVM deployment path.

  • Add virtualization_mode = "kvm" | "pvm" with AENV_VIRTUALIZATION_MODE override.
  • Select mode-specific Firecracker and guest-kernel dependencies during provisioning.
  • Validate the host mode and /dev/kvm prerequisites at startup.
  • Persist the virtualization mode in snapshots and paused-sandbox records and reject cross-mode restore.
  • Publish separate PVM Docker and server-bundle variants while retaining the existing KVM names.
  • Add installer support and a dedicated PVM deployment guide, including matching host-kernel DEB/RPM packages.
  • Deploy multiple versions of the documentation so that the documentation for each release version and the main branch can coexist.

Why

Cloud VMs often do not expose nested virtualization, which prevents the standard KVM deployment from running. PVM provides an alternative deployment path for those environments while keeping standard KVM as the default and recommended mode.

Related issue

Closes #7

Scope and non-goals

Included:

  • Node-level KVM/PVM mode selection.
  • Mode-aware dependency provisioning, host checks, persistence compatibility, release assets, installer behavior, and documentation.
  • Initial PVM support for x86_64.

Not included:

  • Installing or automatically loading the PVM host kernel/module.
  • Cross-mode snapshot or paused-sandbox conversion.
  • Mixing KVM and PVM nodes for the same persisted workload.

Design and behavior changes

  • VirtualizationMode is a shared domain type used by configuration, setup, snapshots, and persistence.
  • The dependency manifest now has symmetric firecracker.{kvm,pvm} and kernel.{kvm,pvm} entries; only the active variant is provisioned.
  • KVM startup rejects a loaded kvm_pvm module. PVM startup requires x86_64, kvm_pvm, and read/write access to /dev/kvm.
  • --setup-only remains host-module independent so release artifacts can be assembled on ordinary CI runners.
  • Legacy snapshots and paused records deserialize as KVM; new records persist their explicit mode.
  • Release artifacts are:
    • aenv-server-linux-x86_64.tar.gz for KVM
    • aenv-server-linux-x86_64-pvm.tar.gz for PVM
    • latest / version tags for KVM images
    • latest-pvm / version-pvm tags for PVM images

Compatibility and operations

  • Public API or generated protocol: No API or generated protocol changes.
  • Configuration or defaults: Adds virtualization_mode; default remains kvm.
  • Snapshot manifest, artifact layout, or storage format: Adds virtualization_mode to committed snapshot metadata and paused-sandbox records. Legacy records default to KVM.
  • Upgrade and rollback: Existing KVM state remains readable in KVM mode. KVM and PVM state is intentionally not interchangeable.
  • Host requirements, permissions, ports, or dependencies: PVM is x86_64-only and requires a compatible PVM host kernel with CONFIG_KVM_PVM, plus an ABI-compatible guest kernel with CONFIG_PVM_GUEST. AgentENV does not install the host kernel automatically.

Validation

  • make fmt
  • make clippy
  • make test-unit
  • Relevant Rust integration tests
  • make -C services test (required when services/ changes)
  • Generated clients/server regenerated with the documented make target
  • Documentation updated
  • Benchmarks or performance comparison completed

Commands and results:

make fmt
make clippy
make test-unit
cargo test -p agentenv --lib setup::kvm::tests -- --nocapture
cargo test -p agentenv --lib setup::deps::tests::pvm_placeholders_fail_before_download -- --nocapture
bash scripts/tests/verify-install-service.sh

All completed successfully. Release, Compose, and Kubernetes YAML files were also parsed successfully, and local Markdown links were checked.

Skipped checks and reasons:

  • make -C services test was not required because no Go service code changed.
  • Generated clients/server were not regenerated because no OpenAPI or generated protocol source changed.
  • Benchmarks were not run because this change does not target runtime performance.

Risks and reviewer notes

  • PVM depends on a non-mainline host kernel and requires explicit operator setup.
  • Host and guest PVM kernels must be ABI-compatible. The packaged guest is based on Linux 6.12.33; matching host-kernel DEB/RPM packages are published from kvcache-ai/linux.
  • Reviewers should focus on mode selection in src/cfg.rs and src/setup/, compatibility checks in snapshot/persistence code, release packaging, and the deployment guide.

Checklist

  • The PR contains one coherent change and no unrelated formatting or refactoring.
  • New behavior is covered by tests, or I explained why testing is impractical.
  • Logs and examples contain no credentials, tokens, or private registry information.
  • I did not manually edit generated code without updating its source and regenerating it.

@github-actions

This comment was marked as outdated.

Comment thread .github/workflows/release.yml Outdated
Comment thread config/default.toml
Comment thread config/deps_manifest.toml
Comment thread config/oss_default.toml
Comment thread scripts/install.sh
Comment thread src/orchestrator/persistence/file_backed.rs Outdated
Comment thread src/orchestrator/service.rs Outdated
Comment thread src/setup/kvm.rs Outdated
Comment thread src/template/builder.rs Outdated
Comment thread .github/workflows/release.yml
Comment thread .github/workflows/release.yml
Comment thread config/default.toml
Comment thread config/deps_manifest.toml
Comment thread config/oss_default.toml
Comment thread src/cfg.rs
.version
.as_deref()
.unwrap_or(&manifest.firecracker.version);
.unwrap_or(&self.manifest_firecracker().version);

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]
The mode now selects the default artifact, but the resolved cache path remains deps/firecracker/<version>/... (and the kernel/helper paths follow the same mode-independent layout). If KVM and PVM use the same explicit version—or future manifest entries reuse a version label—switching modes makes ensure_* accept the existing nonempty artifact and run the binary/kernel from the other backend. Include virtualization_mode in all managed Firecracker/kernel/helper paths (for example, <dependency>/<mode>/<version>) so the compatibility domain is part of the cache key, and update the path tests accordingly.

Comment thread src/orchestrator/persistence/file_backed.rs
Comment thread src/orchestrator/tests.rs
Comment on lines +3463 to +3464
MockBackendFactory::new(),
persister.clone(),

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]
This does not verify the “without mutation” guarantee for backend/runtime side effects. Because the fixture sets paused_state: None and uses an unobserved factory, the resume path cannot build a backend anyway; a regression that performs backend work before checking the virtualization mode is therefore not covered. Give the metadata a MockSnapshot, construct the factory with a MockBehavior hook for MockOperation::BuildFromSnapshot (or a call counter), and assert that the hook is never invoked when the mismatch is returned.

Suggestion:

Suggested change
MockBackendFactory::new(),
persister.clone(),
MockBackendFactory::with_behavior(Arc::clone(&behavior)),
persister.clone(),

Comment thread tests/integration/orchestrator.rs Outdated
Comment thread .github/workflows/release.yml
Comment thread config/default.toml
Comment thread config/deps_manifest.toml
Comment thread config/oss_default.toml
Comment thread scripts/install.sh
Comment thread src/cfg.rs
Comment thread src/orchestrator/persistence/file_backed.rs
Comment thread src/snapshot/types/snapshot.rs
Comment thread src/virtualization.rs
Comment thread config/default.toml
# the DEFAULT_BOOT_ARGS constant in src/sandbox/firecracker/config.rs.
# Keep the DAMON reclaim parameters in sync between both locations.
boot_args = "console=ttyS0 reboot=k panic=1 pci=off init=/init damon_reclaim.enabled=Y damon_reclaim.min_age=60000000 damon_reclaim.quota_ms=100 damon_reclaim.quota_sz=1073741824 damon_reclaim.quota_reset_interval_ms=1000 damon_reclaim.wmarks_high=900 damon_reclaim.wmarks_mid=700 damon_reclaim.wmarks_low=200 damon_reclaim.skip_anon=Y damon_reclaim.wmarks_interval=5000000"
boot_args = "console=ttyS0 reboot=k panic=1 pci=off mitigations=off init=/init damon_reclaim.enabled=Y damon_reclaim.min_age=60000000 damon_reclaim.quota_ms=100 damon_reclaim.quota_sz=1073741824 damon_reclaim.quota_reset_interval_ms=1000 damon_reclaim.wmarks_high=900 damon_reclaim.wmarks_mid=700 damon_reclaim.wmarks_low=200 damon_reclaim.skip_anon=Y damon_reclaim.wmarks_interval=5000000"

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.

[security · medium]
Adding mitigations=off to the default guest command line disables kernel mitigations for CPU vulnerabilities (such as Spectre-class attacks) for every deployment, including the default KVM mode. This weakens isolation between processes running in a guest and is especially risky when guest workloads are untrusted. Keep mitigations enabled by default; if this is required for a specific backend or trusted environment, apply it conditionally or expose it as an explicit opt-in override rather than shipping it in the global default.

Suggestion:

Suggested change
boot_args = "console=ttyS0 reboot=k panic=1 pci=off mitigations=off init=/init damon_reclaim.enabled=Y damon_reclaim.min_age=60000000 damon_reclaim.quota_ms=100 damon_reclaim.quota_sz=1073741824 damon_reclaim.quota_reset_interval_ms=1000 damon_reclaim.wmarks_high=900 damon_reclaim.wmarks_mid=700 damon_reclaim.wmarks_low=200 damon_reclaim.skip_anon=Y damon_reclaim.wmarks_interval=5000000"
boot_args = "console=ttyS0 reboot=k panic=1 pci=off init=/init damon_reclaim.enabled=Y damon_reclaim.min_age=60000000 damon_reclaim.quota_ms=100 damon_reclaim.quota_sz=1073741824 damon_reclaim.quota_reset_interval_ms=1000 damon_reclaim.wmarks_high=900 damon_reclaim.wmarks_mid=700 damon_reclaim.wmarks_low=200 damon_reclaim.skip_anon=Y damon_reclaim.wmarks_interval=5000000"

Comment thread config/deps_manifest.toml

[kernel.pvm]
version = "6.12.33-pvm"
url = "https://github.com/kvcache-ai/linux/releases/download/pvm-kernel-6.12.33/vmlinux-guest-6.12.33-pvm"

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 PVM URL ignores {version}, unlike the manifest contract used by ensure_kernel. If kernel.version is overridden while kernel.url is left unset, setup stores this fixed 6.12.33 image under the overridden version's directory, so configuration and snapshot/dependency identity claim a different kernel than is actually installed. Make the URL derivable from version (adjusting the version/tag naming if necessary), or require a URL whenever a custom version is configured.

Comment thread config/oss_default.toml

[firecracker]
boot_args = "console=ttyS0 reboot=k panic=1 pci=off init=/init"
boot_args = "console=ttyS0 reboot=k panic=1 pci=off mitigations=off init=/init"

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.

[security · medium]
mitigations=off disables the guest kernel's speculative-execution CPU vulnerability mitigations (for example, Spectre/Meltdown mitigations). This configuration can also be used for OSS validation outside a strictly isolated benchmark, so making this unconditional weakens guest security and may expose kernel or cross-process data to untrusted workloads. Please keep mitigations enabled by default and make this an explicit opt-in performance setting for trusted test environments, with the security trade-off documented.

# Builds the server, starts it, creates a base template, then runs each suite.
# Requires: curl, jq, and a Linux host with /dev/kvm.
# Requires: curl, jq, and a Linux host whose /dev/kvm and modules match
# AENV_VIRTUALIZATION_MODE (kvm by default).

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.

[documentation · low]
This newly referenced setting is missing from the environment-variable list directly below, so users are not told the supported values. Add an entry documenting that AENV_VIRTUALIZATION_MODE accepts kvm or pvm and defaults to kvm.

Suggestion:

Suggested change
# AENV_VIRTUALIZATION_MODE (kvm by default).
# AENV_VIRTUALIZATION_MODE - Virtualization backend: kvm or pvm (default: kvm).

Comment thread src/orchestrator/mod.rs
Comment on lines +53 to +57
VirtualizationModeMismatch {
resource: String,
resource_mode: VirtualizationMode,
node_mode: VirtualizationMode,
},

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 new expected compatibility error is not handled by From<OrchestratorError> for models::Error; it falls through to ApiImpl::internal_error and is returned as HTTP 500. For example, creating a sandbox from a snapshot produced in another virtualization mode is a client-visible incompatibility, not an internal server failure. Add an explicit API conversion (for example, 409 Conflict, or 400 for the create request) and an integration test for the response status.

Comment on lines +291 to +293
retained_artifacts.insert(sandbox_id);
sandboxes.push(record.into_metadata_without_runtime_state());
continue;

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]
Stripping paused_state here preserves only the snapshot directory, not the sandbox's image-cache dependencies. During orchestrator startup, restored_paused includes only metadata with paused_state: Some(_), and reconcile_paused_at_startup passes those IDs to reconcile_paused; consequently, the durable cache protection for every foreign-mode record is treated as orphaned and removed. Image maintenance can then collect its rootfs/drive artifacts, so switching the node back to the record's mode may no longer allow this supposedly retained sandbox to resume. Preserve foreign sandbox IDs during protection reconciliation (and ideally persist enough mode-neutral RuntimeArtifactSet data to recreate missing protection) while still withholding resumable backend state.

Comment thread src/orchestrator/tests.rs
Comment on lines +3463 to +3464
MockBackendFactory::new(),
persister.clone(),

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 test claims resume is rejected “without mutation,” but it drops the backend factory and only verifies metadata/persister activity. Consequently, it cannot detect backend construction or lifecycle calls if the compatibility check is later moved after launch begins. Also, paused_state: None prevents the resume path from reaching backend restoration independently of the mode check. Use a valid mock paused state and retain a MockBehavior with a BuildFromSnapshot hook/counter (or a failing action) so the test explicitly proves that no backend operation occurs.

Suggestion:

Suggested change
MockBackendFactory::new(),
persister.clone(),
MockBackendFactory::with_behavior(Arc::clone(&behavior)),
persister.clone(),

Comment on lines +21 to 22
#[derive(Clone, Debug)]
pub struct SnapshotPublishMetadata {

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.

[maintainability · medium]
Removing Serialize and Deserialize is an unnecessary breaking change to this exported type. VirtualizationMode already implements both traits, so the new field does not require dropping Serde support. Downstream users that serialize publication requests or deserialize stored/test fixtures will stop compiling; retain the existing derives unless this API break is explicitly intended.

Suggestion:

Suggested change
#[derive(Clone, Debug)]
pub struct SnapshotPublishMetadata {
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SnapshotPublishMetadata {

Comment thread src/virtualization.rs
Comment on lines +5 to +6
#[serde(rename_all = "lowercase")]
pub enum VirtualizationMode {

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]
Serde and FromStr accept different spellings for the same public type. FromStr trims whitespace and ignores case, but direct Serde consumers such as the new AppConfig.virtualization_mode TOML field accept only exact "kvm"/"pvm"; consequently AENV_VIRTUALIZATION_MODE=KVM (if routed through FromStr) and virtualization_mode = "KVM" can behave differently. Use a custom Deserialize implementation that delegates to FromStr (or make FromStr equally strict), and add tests covering both parsing paths.

Suggestion:

Suggested change
#[serde(rename_all = "lowercase")]
pub enum VirtualizationMode {
pub enum VirtualizationMode {

Comment thread .github/workflows/docs.yml
Comment thread .github/workflows/docs.yml
Comment on lines +73 to +74
if git diff --name-only "$before" "${{ github.sha }}" | grep -qE '^(docs/|src/api/openapi\.yml$|\.github/workflows/docs\.yml$)'; then
echo "should_build=true" >> "$GITHUB_OUTPUT"

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]
With set -o pipefail, grep -q may exit after its first match and close the pipe while git diff is still writing. On a sufficiently large push, git diff receives SIGPIPE, the pipeline becomes nonzero, and a relevant docs build is incorrectly skipped. Avoid the early-closing pipeline, e.g. capture the name list before matching or use Git pathspecs and test whether git diff --quiet "$before" "${{ github.sha }}" -- docs src/api/openapi.yml .github/workflows/docs.yml reports changes.

Comment thread .github/workflows/docs.yml
Comment thread .github/workflows/docs.yml
Comment thread src/orchestrator/persistence/file_backed.rs
Comment thread src/orchestrator/tests.rs
id: sandbox_id,
state: SandboxState::Paused,
virtualization_mode: sandbox_mode,
paused_state: None,

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]
This makes the non-mutation assertion below vacuous: a resume path that clears an existing paused-state payload before returning the mode mismatch would still pass. Initialize this fixture with Some(test_paused_state().clone()), retain the original Arc, and assert that the payload is still present (ideally with Arc::ptr_eq) after the failed resume so the test covers preservation of the data required for a later valid resume.

Suggestion:

Suggested change
paused_state: None,
paused_state: Some(test_paused_state().clone()),

Comment thread src/orchestrator/tests.rs
Comment thread src/setup/deps.rs
return Ok(());
}

let mode_manifest = manifest.firecracker.for_mode(config.virtualization_mode);

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 selected mode affects the download URL, but not the cache identity, and the mode-agnostic fc_path is checked before this selection. If KVM and PVM are configured with the same explicit firecracker.version (but different URLs/artifacts), switching modes will treat the previously downloaded binary as valid and skip downloading the required one. Include virtualization_mode in the managed dependency path/cache key (and apply the same fix to the kernel path), or persist and validate artifact metadata before accepting the cache hit.

Comment thread src/virtualization.rs
@LSX-s-Software

Copy link
Copy Markdown
Member Author

@yingdi-shan 47942e2 introduced a versioned documentation site with three coexisting paths under GitHub Pages, published to the gh-pages branch instead of the previous "Actions artifact" deploy source:

  • /dev/ — preview built from every relevant push to main; shows an "unreleased preview" banner so it's clearly marked as not-yet-released.
  • /latest/ — alias of the most recently tagged (v*) release; this is what the site root / redirects to by default, so release users only ever land on documentation for shipped features.
  • /v{tag}/ — an immutable snapshot published once per release tag and never rebuilt afterwards.
    A JSON manifest (versions.json) tracks known release versions, consumed by an in-page version switcher.

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.

Support PVM-based deployment

1 participant