Skip to content

feat(sandbox): bind running Firecracker threads to host CPUs - #238

Open
emailcannotbeblank wants to merge 1 commit into
kvcache-ai:mainfrom
emailcannotbeblank:feature/bind-cpu-v2
Open

feat(sandbox): bind running Firecracker threads to host CPUs#238
emailcannotbeblank wants to merge 1 commit into
kvcache-ai:mainfrom
emailcannotbeblank:feature/bind-cpu-v2

Conversation

@emailcannotbeblank

@emailcannotbeblank emailcannotbeblank commented Sep 1, 2026

Copy link
Copy Markdown

What

Add runtime CPU-affinity control for running Firecracker sandboxes.

  • Add the admin-only POST /sandboxes/{sandboxID}/cpu-affinity API.
  • Add aenv cpu-bind <sandbox-id> --vcpu <list|*> --core <list>.
  • Support CPU lists with ranges, duplicates, and optional strides such as
    0-10:2.
  • Bind selected vCPU threads, or every current Firecracker thread with *, to
    online host logical CPUs.
  • Verify every affinity update and roll back earlier updates when a later one
    fails.

Why

Hardware architects use AgentENV sandboxes for processor benchmarking and
workload characterization. Runtime CPU affinity supports three main use cases:

  1. Topology-aware performance optimization. Select logical CPUs according
    to LLC, NUMA, and SMT topology to improve long-running workload performance.
  2. Stable measurements. Prevent vCPU threads from migrating across many
    host CPUs, reducing run-to-run noise in PMU counters, IPC, cache-miss rates,
    and latency measurements.
  3. Controlled oversubscription. Place multiple vCPU threads on one logical
    CPU, or on SMT siblings of one physical core, to evaluate oversubscription
    behavior.

Related issue

Closes #228

Scope and non-goals

Included:

  • Runtime affinity changes for running Firecracker sandboxes.
  • Internal Firecracker PID resolution without exposing the PID through the
    public API.
  • CLI/client, gateway routing, admin API, orchestrator, backend, tests, and
    user documentation.

Non-goals:

  • Exclusive CPU reservation or automatic topology selection.
  • Persisting affinity across pause/resume or runtime replacement.
  • Querying current per-thread affinity; this can reuse the process and thread
    abstractions introduced here in a follow-up.
  • Supporting host logical CPU IDs above 1023.

Design and behavior changes

aenv -> gateway -> admin API -> orchestrator -> Firecracker PID
                                               -> /proc thread scan
                                               -> sched_get/setaffinity

The orchestrator accepts only running sandboxes, locks the backend while the
blocking affinity operation runs, and keeps the operation cancellation-safe so
pause, snapshot, or delete cannot replace the Firecracker process midway.

The sandbox layer scans /proc/<pid>/task, identifies vCPU threads by their
fc_vcpu N names, and checks thread start times around numeric-TID syscalls to
narrow the TID-reuse window. Input length, expansion, value count, and CPU IDs
are bounded before allocation. Requested offline CPUs are ignored; an empty
online intersection is rejected before any affinity change.

Before applying changes, the implementation records every target's original
affinity. It then updates and reads back each thread one at a time. A failure or
kernel mismatch triggers best-effort rollback of all threads already changed.

Compatibility and operations

  • Public API or generated protocol: Adds one admin-only POST endpoint and its
    OpenAPI-generated server types. Existing endpoints are unchanged.
  • Configuration or defaults: No new configuration. The endpoint is registered
    by default and protected by the deployment API key; there is no separate
    feature flag.
  • Snapshot manifest, artifact layout, or storage format: N/A; affinity is live
    kernel state and is not persisted.
  • Upgrade and rollback: No data migration is required. Rolling back removes the
    API, but an already-applied affinity remains until changed or the Firecracker
    process exits.
  • Host requirements, permissions, ports, or dependencies: Linux /proc and
    permission to call sched_setaffinity on Firecracker threads. No new port or
    runtime dependency is introduced. Supported logical CPU IDs are 0-1023.

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
  passed

make clippy
  passed: workspace, all targets, all features, warnings denied

make test-unit
  passed

cargo test -p agentenv cpu_affinity --lib
  passed: 14 passed, 0 failed, 1 ignored helper process

AENV_UBLK_DAEMON_METRICS_LISTEN_ADDR="" make test-agent-integration
  passed: AgentENV integration suites and snapshot OSS tests
  snapshot OSS result: 4 passed, 0 failed

make -C services test
  passed: gateway and scheduler tests

Skipped checks and reasons:

  • make agentenv-server: generated server changes and their OpenAPI source are
    committed, but final regeneration could not be rerun because Java is not
    installed in the validation environment.
  • Benchmarks: not run; this adds an explicit administrative control path rather
    than changing a request-processing hot path.
  • End-to-end tests: not run because they require a privileged KVM/ublk runtime
    environment.

Risks and reviewer notes

  • Incorrect placement can reduce performance for other workloads on the node;
    the endpoint is therefore admin-only.
  • Affinity limits scheduler eligibility but does not reserve CPUs or provide
    exclusive access. All selected threads receive the same host CPU set.
  • * selects threads present when the request is handled; it is not a policy
    for threads created later.
  • Affinity is not restored after pause/resume, because resume creates a new
    Firecracker process.
  • Numeric PID/TID syscalls cannot eliminate reuse races completely. Start-time
    checks before and after affinity reads narrow that window.
  • Rollback is best-effort and reports any thread that could not be restored.

Suggested review order:

  1. src/sandbox/cpu_affinity.rs for parsing, thread selection, verification,
    and rollback.
  2. src/orchestrator/service.rs for lifecycle locking and PID handling.
  3. src/api/openapi.yml and src/api/impls/admin.rs for the admin API.
  4. services/gateway/internal/server.go and the aenv client/command for
    routing and user-facing behavior.

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.

Add an admin-only CPU-affinity API and the `aenv cpu-bind` command. Bind selected vCPU threads, or all Firecracker threads with `*`, to host logical CPUs.

Validate bounded CPU lists with ranges and strides, ignore offline CPUs, verify each update, and roll back earlier updates on failure.
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

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

  • ✅ Successfully posted inline: 4 comment(s)

Comment on lines +154 to +157
let resp = handle_status(
self.post(&format!("/sandboxes/{id}/cpu-affinity"))
.send_json(&body),
)?;

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
This uses Client::post, which authenticates with X-API-Key, but the new OpenAPI operation declares only AdminApiKeyAuth (X-Admin-Token). Consequently, aenv cpu-bind created via Client::from_env() will receive 401 even with the normal saved API key. Either expose this operation under the intended regular/team authentication with the necessary ownership checks, or add an explicit admin credential/header path to the client and command.

Comment on lines +799 to +805
let pid = sandbox.runtime_process_id().map_err(|source| {
OrchestratorError::SandboxOperationFailed {
sandbox_id,
operation,
source,
}
})?;

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
The handle lock prevents orchestrator-driven replacement, but it does not establish that this numeric PID still belongs to this Firecracker process. If Firecracker exits naturally and the PID is reused before bind_process_cpu_affinity scans /proc, a wildcard request can enumerate and change every thread of an unrelated host process. The per-thread starttime checks only preserve the identity discovered during that scan; they do not compare it with the original Firecracker process identity. Keep a stable process reference (for example, a pidfd) or capture and validate the Firecracker leader's start time before applying any affinity changes.

Comment on lines +416 to +418
fn runtime_process_id(&self) -> Result<i32> {
Ok(self.fc_instance.pid()?.as_raw())
}

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 · high
This exposes only the numeric PID from Child::id(), which can remain available after Firecracker exits. If the PID is reused before the blocking worker scans /proc, affinity can be applied to an unrelated host process; the per-thread start-time checks only establish identities after that stale process PID has been accepted. Return a stable runtime identity (for example, retain a pidfd plus the process start time captured at spawn) and verify the original process is still alive/owns this PID for the duration of binding, rather than passing a bare i32 across the operation.

Comment thread src/sandbox/mock.rs
Comment on lines +343 to +345
fn runtime_process_id(&self) -> Result<i32> {
i32::try_from(std::process::id()).context("mock runtime pid does not fit in i32")
}

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
A successful vcpu="*" request against this mock binds every thread discovered in the test runner process, and the orchestrator path does not restore affinity after success. This violates the mock backend's no-op isolation and can permanently constrain unrelated concurrent tests. The mock should report that runtime CPU affinity is unsupported, or own a disposable helper process whose PID can safely be returned.

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.

cpu-bind wanted

1 participant