ADR-0001: Control plane and deployment model
- Status: Accepted (milestone 1) for the embedded and self-hosted modes. The hosted mode is Proposed.
- Date: 2026-10-29
Context
GPUlayer must work on a laptop with no account and no service, and must also coordinate many remote, cloud and distributed workers. CLAUDE.md asks for logical boundaries without premature microservices. It also asks that a "hosted" control plane never imply that user data is sent to a GPUlayer-operated service.
Decision
- There is a single
ControlPlaneclass (src/gpulayer/control_plane/service.py). It owns the job lifecycle, worker registry, leases, scheduling passes, distributed coordination and provisioning (throughcontrol_plane/provisioning.py). Scheduling is a separate pure module (control_plane/scheduler.py; see ADR-0004). - Embedded mode. The SDK (
sdk/client.py) and CLI openControlPlanein-process over$GPULAYER_HOME/metadata.db(sdk/home.py). Local workers talk to it throughEmbeddedChannel(worker/channel.py). Several local processes, such as the CLI andgpulayer worker start, share the SQLite file. No network listener is opened. - Self-hosted mode.
gpulayer server startwraps the same object inControlPlaneServer(api/server.py, stdlib HTTP). Clients useHttpControlPlaneClientand workers useHttpWorkerChannel(api/client.py). - The worker agent (
worker/agent.py) is identical in both modes; only the channel differs. - Reconciliation, meaning lease and heartbeat expiry, deadlines and provisioning, runs opportunistically on API calls and every second in a server background thread.
- Hosted mode (Proposed). This would be the same contracts operated as a managed service. Nothing hosted exists. No telemetry is sent anywhere.
Alternatives considered
- Separate services (API, scheduler, registry, coordinator): this adds deployment and consistency complexity before any scale requires it.
- Local-only runner first, server later: this risks two divergent code paths, which CLAUDE.md forbids.
- A Kubernetes/Ray/Slurm-based control plane: this is a heavy dependency for the local case. It may become a provider or worker backend later.
Consequences and trade-offs
- Local, remote and cloud jobs share one code path, and the tests exercise it in both modes.
- The single-process server is a single point of failure, and availability equals that of one host. There is no HA.
- SQLite limits write concurrency. All writes serialize on one lock per process
(
storage/sqlite.py), which is adequate for tens of workers but not for thousands. - Known gap: some provider calls (
provision,terminate) run while the SQLite transaction lock is held:schedule_pendingcallsProvisioningManager.request, and_set_job_statecalls_finalize, which callsrelease_for_job, all insidestore.tx(). A slow provider API would stall the control plane. This must change before a real provider ships (ADR-0010).
Security and operational implications
- Embedded mode trusts the OS user: anyone who can write
$GPULAYER_HOMEcontrols jobs. The directory is created with mode 0700. - Served mode authenticates every route except
/v1/health(ADR-0003). - What reaches the control plane: job specs, logs, events, artifacts (source bundles, inputs, outputs) and worker capabilities. Secret values never do (ADR-0009).
Follow-up
- Move provider I/O outside DB transactions (outbox or queued commands).
- Choose a shared database for multi-replica deployments (ADR-0008).
- Write the hosted-mode ADR, covering tenancy, data residency and isolation (ADR-0009).