Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 65 additions & 34 deletions crates/pantograph-embedded-runtime/src/serial_cpu_residency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,40 @@ struct ShutdownState {
}

impl EmbeddedSerialCpuResidency {
/// Construct the dedicated fair serial CPU residency owner with workload-key
/// reuse disabled. Existing resource and lifecycle requirements apply.
pub async fn new(
config: EmbeddedSerialCpuResidencyConfig,
) -> Result<Self, EmbeddedRuntimeError> {
Self::build(config, false).await
}

/// Opt into bounded pure workload-key reuse on the ordinary per-member path.
///
/// Each member retains its own fair opportunity, preparation, resource and
/// Start checks, native execution/drain and matched cleanup. This enables no
/// native batching, new admission policy or complete-memory guarantee.
/// Construction does not load a model or qualify execution/performance.
pub async fn new_with_bounded_key_reuse(
config: EmbeddedSerialCpuResidencyConfig,
) -> Result<Self, EmbeddedRuntimeError> {
Self::build(config, true).await
}

async fn build(
mut config: EmbeddedSerialCpuResidencyConfig,
bounded_key_reuse: bool,
) -> Result<Self, EmbeddedRuntimeError> {
config.validate()?;
let mut serial = WorkflowSerialReadyConfig::default()
.with_fair_service_opportunities()
.with_serial_residency()
.map_err(initialization_error)?;
if bounded_key_reuse {
serial = serial
.with_bounded_grouping()
.map_err(initialization_error)?;
}
config.runtime.max_loaded_sessions = Some(config.runtime.max_loaded_sessions.unwrap_or(8));
let calibration = inference::CandleCpuCalibrationConfig::new(
config.cpu_threads,
Expand Down Expand Up @@ -214,42 +244,36 @@ impl EmbeddedSerialCpuResidency {
.map_err(initialization_error)?,
);
std::fs::create_dir_all(&config.runtime.app_data_dir).map_err(initialization_error)?;
let service = WorkflowService::new_serial_ready_cpu(
port.clone(),
WorkflowSerialReadyConfig::default()
.with_fair_service_opportunities()
.with_serial_residency()
.map_err(initialization_error)?,
)
.with_attribution_store(
pantograph_runtime_attribution::SqliteAttributionStore::open_in_memory()
.map_err(initialization_error)?,
)
.with_diagnostics_ledger(
SqliteDiagnosticsLedger::open(
config
.runtime
.app_data_dir
.join("serial-cpu-diagnostics.db"),
let service = WorkflowService::new_serial_ready_cpu(port.clone(), serial)
.with_attribution_store(
pantograph_runtime_attribution::SqliteAttributionStore::open_in_memory()
.map_err(initialization_error)?,
)
.map_err(initialization_error)?,
)
.with_artifact_store(
ArtifactStore::open(
config.runtime.app_data_dir.join("serial-cpu-artifacts"),
ArtifactPolicy {
policy_id: "serial-cpu-residency".into(),
policy_version: 1,
ttl_seconds: None,
max_disk_bytes: Some(1024 * 1024),
max_memory_bytes: Some(1024 * 1024),
max_single_artifact_bytes: Some(65536),
spill_threshold_bytes: None,
delete_on_consume: false,
},
.with_diagnostics_ledger(
SqliteDiagnosticsLedger::open(
config
.runtime
.app_data_dir
.join("serial-cpu-diagnostics.db"),
)
.map_err(initialization_error)?,
)
.map_err(initialization_error)?,
);
.with_artifact_store(
ArtifactStore::open(
config.runtime.app_data_dir.join("serial-cpu-artifacts"),
ArtifactPolicy {
policy_id: "serial-cpu-residency".into(),
policy_version: 1,
ttl_seconds: None,
max_disk_bytes: Some(1024 * 1024),
max_memory_bytes: Some(1024 * 1024),
max_single_artifact_bytes: Some(65536),
spill_threshold_bytes: None,
delete_on_consume: false,
},
)
.map_err(initialization_error)?,
);
let input = EmbeddedHostedStartupCompositionInput::new(EmbeddedHostedStartupConfig {
runtime_registry: registry.clone(),
runtime_registry_controller: gateway.clone(),
Expand Down Expand Up @@ -303,6 +327,13 @@ impl EmbeddedSerialCpuResidency {
})
}

/// Read immutable constructor facts without querying native state or opening
/// execution admission. This value remains readable after shutdown and is
/// neither an execution capability nor proof of readiness or qualification.
pub fn scheduler_configuration(&self) -> WorkflowSchedulerConfiguration {
self.runtime.workflow_service.scheduler_configuration()
}

async fn admit(&self) -> Result<tokio::sync::RwLockReadGuard<'_, ()>, WorkflowServiceError> {
let guard = self.operations.read().await;
if self.closed.load(Ordering::Acquire) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,55 @@ async fn invalid_facade_configuration_refuses_before_owner_or_filesystem_startup
assert!(!root.exists());
}

#[tokio::test]
async fn key_reuse_configuration_refuses_before_owner_or_filesystem_startup() {
let directory = tempfile::tempdir().unwrap();
let root = directory.path().join("not-created");
let mut configuration = config(&root);
configuration.ram_capacity_bytes = 0;
assert!(matches!(
EmbeddedSerialCpuResidency::new_with_bounded_key_reuse(configuration).await,
Err(EmbeddedRuntimeError::Config { .. })
));
assert!(!root.exists());
}

#[tokio::test]
async fn owning_facade_explicit_key_reuse_preserves_default_and_shutdown_configuration() {
for enabled in [false, true] {
let directory = tempfile::tempdir().unwrap();
seed(directory.path());
let configuration = config(directory.path());
let facade = if enabled {
EmbeddedSerialCpuResidency::new_with_bounded_key_reuse(configuration).await
} else {
EmbeddedSerialCpuResidency::new(configuration).await
}
.unwrap();
let snapshot = facade.scheduler_configuration();
assert_eq!(
snapshot.execution_owner(),
WorkflowSchedulerExecutionOwner::SerialReadyCpu
);
assert!(snapshot.serial_fair_opportunities());
assert!(snapshot.serial_residency());
assert_eq!(snapshot.serial_key_reuse(), enabled);
assert!(!snapshot.serial_completion_advisory());
assert_eq!(snapshot.serial_minimum_samples(), Some(3));
assert_eq!(
snapshot.serial_maximum_sample_age(),
Some(Duration::from_secs(60))
);
assert!(facade.registry_snapshot().reservations.is_empty());
assert_eq!(facade.residency_diagnostics().unwrap().loads, 0);
assert_eq!(facade.scheduler_configuration(), snapshot);
facade.shutdown().await.unwrap();
facade.shutdown().await.unwrap();
assert_eq!(facade.scheduler_configuration(), snapshot);
assert!(facade.registry_snapshot().reservations.is_empty());
}
}

#[tokio::test]
async fn projection_refresh_publishes_existing_raw_evidence_without_native_work() {
use pantograph_diagnostics_ledger::{
Expand Down
30 changes: 30 additions & 0 deletions docs/cpu-scheduler-host-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,36 @@ builders remain explicit and require fair service opportunities; observing their
flags does not promote the unresolved M4/C2 acceptance gates. M6a adds no
application selector, UI, IPC field, persisted preference or provider capability.

## Protected embedded residency owner

With `pantograph-embedded-runtime`'s `backend-candle` feature, the owning
`EmbeddedSerialCpuResidency` facade provides two construction choices:

- `new(config)` preserves fair serial residency with workload-key reuse disabled.
- `new_with_bounded_key_reuse(config)` explicitly enables the existing bounded
pure workload-key cache on that same per-member execution path.

Both take the existing `EmbeddedSerialCpuResidencyConfig` unchanged. Invalid
resource/configuration declarations refuse before filesystem/native startup.
The caller still supplies valid local model identities and operator-declared
resource bounds; construction loads no model and downloads none. Physical run
and resume retain their `host-dependency-inventory` feature requirement.

`facade.scheduler_configuration()` returns the same immutable value snapshot
described above, with fair opportunities/residency true, key reuse matching the
constructor, and completion advice false. It exposes no service, gateway,
registry, native owner or execution authority. It remains readable after
shutdown because configuration is not live runtime state.

This is immediate ordinary member execution with planning reuse, not a grouped
native forward. Each member retains its own fair opportunity, preparation,
reservation, final Start checks, drain and matched cleanup. Change this choice
by acknowledged facade shutdown followed by construction at the next clean
startup; there is no live setter. Admission, resource accounting and defaults
are unchanged. Constructor/consumer tests do not qualify actual CPU benefits;
the [finite completion checklist](plans/cpu-scheduler/reports/2026-10-21-m5-m6-completion-checklist.md)
keeps that native comparison and representative performance evidence separate.

## Lifecycle and rollback

The snapshot is stable across service clones and loaded-session capacity changes.
Expand Down
47 changes: 47 additions & 0 deletions docs/plans/cpu-scheduler/execution-ledger.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,52 @@
# CPU Scheduler Execution Ledger


## 2026-10-21 — PR89 integration and finite M5/M6 consuming path

- User authorized normal PR89 integration and required a complete remaining-plan
checklist before more allocation abstractions. Fresh actor MrScripty, remote
main/source tips, exact head48717570/tree2d59d369, 22 successful check runs/four
PR workflows and empty GitHub review/thread arrays were verified. Independent
source reviewer extended approval to merge; CodeRabbit supplies no source credit.
- Ready transition followed by guarded normal merge succeeded as dc64b895, parents
dc926783 and48717570, exact approved tree2d59d369. Source branches remain intact.
Actual-main post-merge CI is pending; do not infer its result from source CI.
- Independent audit found full allocator hooks are not an M5 prerequisite for
cost-free ordinary-member key reuse. The integrator explicitly selects that
previously open C2 option as the supported CPU envelope, retaining the actual
native comparison and broader qualifications. The [finite checklist](reports/2026-10-21-m5-m6-completion-checklist.md)
sets five deciding rows and stops further diagnostic slices from substituting
for usable consuming behavior or actual CPU evidence.
- Both independent pre-code reviewers admitted the additive owning constructor
and direct immutable query with unchanged config literals/defaults/owners/
feature gates/lifecycle. Source write set is serial_cpu_residency.rs and its
existing tests; documentation is the host guide and CPU plan/ledger/issues/
checklist. No allocator, dependency, manifest/lockfile, binding, IPC, CI or
admission change. Independent exact-source review approved the consumer change.
- Local verification passed: default facade module8, full embedded library607
with2 existing ignored cost probes, inventory facade module6 including the
existing native lifecycle case; all-feature/all-target own-package warning-deny
Clippy, warning-deny own-package Rustdoc, formatting and33 local doc links.
Declared pre-push tests passed261/one existing ignored+168.
- Initial no-default/Candle test compilation selected a profile whose existing
tests require llama Gateway::new, and found one corrected new-test Option access;
no tests ran. The first default execution passed2/refused6 on the restricted
default Pumas registry home path. Both failures are retained. The pinned Pumas
supported PUMAS_REGISTRY_DB_PATH override points process-local test metadata to
writable scratch; corrected default/inventory runs passed without HOME,
authentication or security changes. No duplicate tests were launched during
the brief exec transport interruption; recovery was verified with pwd.
- Exact published head/tree and hosted CI belong to the draft PR. Constructor
facts and existing native regression execution do not close M5-CPU; that fixed
qualification producer/protocol and one retained comparison remain next after
this consumer's integration.
- New isolated branch/worktree feat/cpu-scheduler-owned-key-reuse at
/workspace/pantograph-owned-key-reuse starts from verified main dc64b895.
Root agent owns integration; target main. Retain it protected for parent review;
no worktree or prior branch is eligible for cleanup under this task.
- PR89 baseline strict Rustdoc/narrow-feature lint limitations and prior Forbidden
log evidence remain preserved, unsuppressed and unretried.

## 2026-10-21 — PR88 integration and result-payload foundation

- User explicitly authorized normal PR88 merge and the next reviewed backend
Expand Down
6 changes: 4 additions & 2 deletions docs/plans/cpu-scheduler/issues.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ copy/PUMAS/stop. Retained shutdown supervision survives caller wait drop. These
are bounded implementation corrections, not default-policy or recovery expansion.

| CPU-34 | The Ready boundary lacked a provenance-aware historical warm-service producer and inclusive measurements covering capture/cloning, bounds/equality and key work. Encoding counts alone establish no total-overhead result. | The bounded producer merged normally as PR88/dc926783 with exact approved tree88aaf720 after independent review, 22 successful check runs and four successful PR workflows. Local1,047-case evidence and the hosted84-case selection remain distinct. Admission and key-work counts stay unchanged. See [contract/evidence](reports/2026-10-21-provenance-producer.md). |
| CPU-35 | Pinned Candle has no inspected complete CPU allocation-lifetime observer: tensor IDs alias storage, while BERT temporaries, GEMM scratch and Rayon attribution escape returned-buffer snapshots. | This is missing software instrumentation, not a hardware-only blocker. The reviewed completed-result foundation covers only original immutable output Vec payload ownership after materialization, with explicit bookkeeping windows. No placeholder memory API or reservation/RSS arithmetic is added. Separately review dependency-level allocation/drop, shared-storage, scratch and worker attribution before any complete incremental-memory claim or admission use; representative model/hardware evidence qualifies the implemented producer later. |
| CPU-35 | Pinned Candle has no inspected complete CPU allocation-lifetime observer: tensor IDs alias storage, while BERT temporaries, GEMM scratch and Rayon attribution escape returned-buffer snapshots. | This is missing software instrumentation, not a hardware-only blocker. The reviewed completed-result foundation covers only original immutable output Vec payload ownership after materialization, with explicit bookkeeping windows. No placeholder memory API or reservation/RSS arithmetic is added. Dependency allocation/drop, shared-storage, scratch and worker attribution remains required for a future complete incremental-memory claim or admission use. It is not required for the cost-free C2a path selected by the finite completion checklist; no further allocator slice is next. Representative model/hardware evidence qualifies future claims later. |

| CPU-36 | Original completed embedding-result payload can remain allocated in an unpolled join or surviving Shared/Arc reader after worker exit/drain; wrapper completion is not final storage retirement. | Reviewed safe-Rust foundation attaches one lease inside the blocking worker to the immutable completed result and releases after actual payload drop. Four fixed job-owner counters measure only observed retained Vec capacity. Complete allocator/attempt memory remains unavailable; existing admission and cleanup authority stay unchanged. See [contract](reports/2026-10-21-result-payload-attribution.md). |
| CPU-36 | Original completed embedding-result payload can remain allocated in an unpolled join or surviving Shared/Arc reader after worker exit/drain; wrapper completion is not final storage retirement. | PR89 merged normally as dc64b895 after independent merge approval and 22 successful source check runs/four PR workflows; post-merge CI is pending. The reviewed safe-Rust foundation attaches one lease inside the blocking worker to the immutable completed result and releases after actual payload drop. Four fixed job-owner counters measure only observed retained Vec capacity. Complete allocator/attempt memory remains unavailable; existing admission and cleanup authority stay unchanged. See [contract](reports/2026-10-21-result-payload-attribution.md). |

| CPU-37 | The protected `EmbeddedSerialCpuResidency` consumer hardcodes fair+residency with key reuse off and exposes no immutable configuration query, leaving the existing C2a opt-in inaccessible through that safe owner. | Add one explicit key-reuse constructor and delegate the existing value query, preserving defaults, config literals, private owners and shutdown. The [finite checklist](reports/2026-10-21-m5-m6-completion-checklist.md) owns remaining consumer and actual CPU gates; complete allocator diagnostics are not a substitute. |
Loading
Loading