22 KiB
Resource Design: Decisions, Proposals and Open Questions
The persistent discussion log for the unified resource model
(resource-model.md) and the provisioning work (resource-provisioner,
provisioning-demand, elastic-*). Specs must not settle an item here
silently.
How to use:
- Status values:
open(not discussed),discussing(user input recorded, not final),leaning(direction agreed, details open),decided,deferred(later, with a reason). - Changes: update an item in place and add a dated line under "History". When an item is decided, also update the documents it names.
- IDs: R = resource model, P = provisioner, N = new questions raised in discussion, PR = proposals.
Proposals under discussion (user, 2026-10-11)
PR-1: one resource map for all supply, including provisioned supply (status: leaning)
- User: should
resource-distribution-mappingbe made generic, so it also handles resource provisioning? - Analysis (2026-10-11): yes, for the data model.
- The resource map becomes the single record of supply: for each resource, which locations (host, host group, pool, storage) hold how much.
- Each entry has a source (static config, reporter, provisioner) and a state (requested, available, draining, releasing).
- Provisioning only changes map entries over time, e.g. a license pool's base plus borrowed amount, or a new dataset copy.
- The provisioning mechanism stays in the provisioner: policy, provider calls, leases, epochs, orphan scans. The map is the state; the provisioner is the actuator. This is the Kubernetes split too (drivers publish ResourceSlices; autoscalers and provisioners act).
- The scheduler keeps reading only the map's cached snapshot.
- Affects:
resource-distribution-mapping,elastic-shared-resources,resource-provisioner,provisioning-demand. - Open: see N1, N2.
PR-2: a resource provider proxy between the controller and upstream providers (status: leaning)
- User: introduce a resource provider that proxies requests and states between the XCE controller and upstream resource providers.
- Analysis (2026-10-11): yes. It generalizes the
resource-provisionerdaemon. The proxy:- connects to the controller as one role over libxce/Cap'n Proto;
- speaks each upstream provider's protocol: the JSON script contract, a persistent JSON-lines mode, later CSI or vendor APIs;
- holds provider sessions, credentials and rate limits, and translates provider states into map entries and claim states;
- publishes inventory and values (it is the external reporter of R2);
- carries out provisioning and claim operations with
op_id, an epoch and owner tags.
- Why it helps: the controller never blocks on or depends on an outside system, and credentials never reach the controller.
- Where node-local work runs: GPU binding, mounts and scratch stay on the worker, possibly behind the same contract.
- Affects:
resource-provisioner(rename or generalize),custom-resource-reporting(external reporters),resource-claims(storage providers), R4/C3 (the CSI adapter would live here). - Open: see N2, N4.
PR-3: datasets in the resource map; staging as data provisioning (status: discussing)
- User: could data instances be handled by the resource distribution map, and data staging by provisioning the data to a new location?
- Analysis (2026-10-11): this fits PR-1 well.
- Datasets: a dataset is a resource whose map entries are the locations holding a copy. Locality is an L1 attribute the scheduler can use.
- Staging: staging is provisioning a copy at a new location, with a lead time like any provider.
- Eviction: evicting a copy is a release.
- Shortage: "dataset X needed at host group Y" can become provisioning demand.
- Locality, caching and stage-in become one mechanism.
- Caveats:
- Job-private data (stage-out results, scratch, a job's own outputs) is still a per-job claim (
resource-claims), not shared supply. - Datasets need an identity and version, capacity accounting on the storage holding a copy, an eviction policy, and consistency rules (read-only vs updated).
- Job-private data (stage-out results, scratch, a job's own outputs) is still a per-job claim (
- Affects:
resource-distribution-mapping,custom-resource-reporting(string-set locality may become map entries instead),resource-claims(scope shrinks to job-private data), R9. - Open: see N3.
PR-4: hosts as supply entries; the worker connection only confirms them (status: discussing)
- User: hosts get into the controller directly today; how could they be unified with the rest of the supply?
- Today: a worker connects to the controller and is accepted only if its name matches a host config entry (
find_matched_hostconfig,src/controller/join.c:~76). The host record is created by that first join (init_worker).elastic-hostsplans "expected host" records matched at join. - Proposal: separate who decides a host should exist (control plane) from the worker's connection (data plane):
-
Host supply entries: a host is an entry in the supply map with a source and a lifecycle state:
config: a statichosts.yamlentry or pattern;provisioned: created by a proxy as an expected host, with instance id, template and deadline;discovered: an unknown host that joins, admitted only if a policy allows it.
Lifecycle:
expected → joining → available → draining → released, pluslost. -
Join confirms: the worker still connects directly. Job control, heartbeats and resync stay on that long-lived connection. A join confirms an existing entry (or is admitted by policy) instead of being the only way a host appears.
-
Host resources:
slots,memand so on stay L2 counters at the host's location, as today. -
Bring-up:
xce-node-managercan act as the node-side provider that starts the worker on a provisioned machine.
-
- Gains: one place for static, elastic (cloud) and discovered hosts. Orphan rules apply uniformly (expected but never joined; joined but unknown).
provisioning-demandsees hosts and other resources the same way. - Affects:
elastic-hosts(its expected-host records become supply entries),resource-distribution-mapping(PR-1),xce-node-manager, the controller join path.
PR-5: simplified dataset model, v1 (status: discussing)
- User: give a simplified dataset model first.
- Proposal:
- Identity: a dataset is
name@versionand is immutable; a change is a new version. No update consistency problem. - Copies: supply-map entries of the dataset at a location (host, host group or storage pool) with a state
staging → available → evictingand a size. The size is charged as an L2 counter on the location's cache or scratch capacity. - Jobs: a job names datasets, e.g.
data[imagenet@3]. The scheduler prefers hosts with an available copy, as a locality attribute from its cached snapshot. v1 does not make placement wait for a copy. - Staging: in v1 staging is explicit. A user or admin asks for a copy at a location (
xcecli data stage); a proxy provider copies it and reportsavailable. Eviction is explicit too. - Later: scheduler-driven staging (shortfall as provisioning demand), automatic LRU eviction, dataset quotas, and writable datasets.
- Identity: a dataset is
- Open: whether v1 job placement should require a copy (a hard constraint) or only prefer one; and what the dataset registry is (config, REST object, or both).
PR-6: job input/output through provisioned volumes (status: discussing)
- User (2026-10-11): use volume provisioning for job input and output.
- Context:
- bug-1936 rejects
-i/-o/-e,out_dirandtransfer_filesat submit, because no field carries them to the job and the worker never stages files. - bug-1937 defers stage-in/out to a future spec.
- Worker file fetch is locked to its own output path (bug-1508).
- bug-1936 rejects
- Proposal: job I/O as claims (
resource-claims):- Input claims (read-only): from a dataset copy (PR-5), for large or shared inputs; from files uploaded at submit, which a provider puts into a per-job input volume (the new home of
transfer_files/-f); or from an existing standalone claim (model weights, a previous job's output). - Output claim (read-write): per job (template) or a named standalone claim.
-o/-eandout_dirresolve inside it.- Results are fetched through the API server from the provider, not the worker (this removes the bug-1508 restriction).
- Retention policy: keep until fetched, or N days.
- Optional export after the job (stage-out) to another volume, object storage or the user's home directory.
- Placement in the job: the sandbox mounts the claims at fixed points (
/xce/in,/xce/out) or at profile-allowed paths. Direct-mode jobs get the paths through environment variables. - v1 provider: a directory provider on a shared filesystem. Create is
mkdirwith an owner and a quota, publish is a bind mount or path, delete is removal. CSI, cloud block and burst-buffer volumes plug into the same contract later. - Default: jobs without I/O options keep today's behaviour.
- Input claims (read-only): from a dataset copy (PR-5), for large or shared inputs; from files uploaded at submit, which a provider puts into a per-job input volume (the new home of
- Refinement (user, 2026-10-11): a shared volume with per-job subdirectories is the default (status: leaning). Jobs do not each get a volume; several jobs share one:
shared_volume/<user>/<jobid>/{in,out,err}, array elements<jobid>/<index>/....- The shared volume: a long-lived, admin-provisioned standalone claim (read-write-many, retain). Its capacity is a pool counter in the supply map; the hosts that mount it are an L1 attribute, so placement only uses hosts that reach it.
- A job's directory: a lightweight template claim of kind "subdirectory". Create is
mkdirplus ownership, with no storage call; release is retention or cleanup. - Per-job volumes (burst buffer, large scratch) stay possible when a profile asks for one.
- Settles: N9 (read-write-many, so multi-host jobs work) and N10 (a subdirectory per element). Reframes N8 as per-user or per-project quota on the shared volume (filesystem quota, or usage charged to the pool counter after the fact, consistent with
max(reserved, actual)). - Prior art: Kubernetes
subPathand the nfs-subdir provisioner; the shared output trees common at LSF and Slurm sites.
- Affects:
resource-claims(adds job I/O as its first use, ahead of checkpoints);job-sandbox-execution(mount points);- the apiserver submit/fetch path (lifts the bug-1936 rejections once claims exist);
application-profile(default I/O claims, allowed paths);- PR-5 (inputs from datasets).
CSI notes (2026-10-11, for C3 and R4)
Source: the CSI specification, from general knowledge; to be checked against the current spec version during design.
- What it is: the Container Storage Interface, a CNCF specification: a gRPC API between a container orchestrator and a storage driver. It is orchestrator-neutral (used by Kubernetes, Nomad, Mesos and Cloud Foundry), though most drivers are written, packaged and tested for Kubernetes.
- Three services:
- Identity (every plugin):
GetPluginInfo,GetPluginCapabilities,Probe. - Controller (runs once, central):
CreateVolume/DeleteVolume,ControllerPublishVolume/Unpublish(attach to a node),ValidateVolumeCapabilities,ListVolumes,GetCapacity,CreateSnapshot/DeleteSnapshot/ListSnapshots,ControllerExpandVolume,ControllerGetVolume. - Node (one per host, privileged):
NodeStageVolume/Unstage(mount once per node at a staging path),NodePublishVolume/Unpublish(bind-mount into the workload's path),NodeGetInfo(node id, max volumes, accessible topology),NodeGetCapabilities,NodeExpandVolume,NodeGetVolumeStats.
- Identity (every plugin):
- Key concepts:
- Volume capability: block or filesystem, plus fs type and mount flags.
- Access modes: single-node writer or reader, multi-node reader-only, single-writer or multi-writer.
- Topology: driver-reported segments such as
zone=aorrack=r1;CreateVolumetakes requisite and preferred topology. - Opaque parameters (a storage class); per-call secrets; volume context returned by create.
- How Kubernetes runs it:
- The controller plugin runs as a Deployment, with sidecars (external-provisioner, attacher, snapshotter, resizer) that turn PVCs into CSI calls.
- The node plugin runs as a privileged DaemonSet, with node-driver-registrar, over a unix socket.
- The sidecars are Kubernetes-specific; the CSI calls themselves are not.
- Mapping to XCE:
- Controller side → PR-2 proxy:
CreateVolumewhen a claim or dataset copy is created;ControllerPublishwhen it is bound to hosts;Deleteon release;GetCapacity→ pool counters in the map; snapshots → checkpoint snapshots; secrets stay in the proxy. - Node side → worker/xcecd:
NodeStageon first use on a host;NodePublishinto the job's sandbox (or container) path; unpublish and unstage at job end.NodeGetInfotopology → host attributes (L1). - Timing: CSI topology plus "create after placement" matches the rule that the scheduler only selects (Kubernetes
WaitForFirstConsumer). - Access modes map one-to-one onto claim access modes.
- Controller side → PR-2 proxy:
- Costs:
- gRPC clients: natural in Go (the proxy; P2), awkward in C (the worker). A small Go node helper, started with
posix_spawnor run underxce-node-manager, could make the node calls. - The node plugin is a privileged daemon on every host, needing mount propagation.
- Some drivers assume Kubernetes APIs or container packaging, so each target driver needs testing outside Kubernetes.
- gRPC clients: natural in Go (the proxy; P2), awkward in C (the worker). A small Go node helper, started with
- Relevant drivers:
- HPC parallel filesystems: IBM Spectrum Scale/GPFS CSI, BeeGFS CSI, Lustre (e.g. AWS FSx, DDN), WEKA, VAST.
- General storage: NFS, Ceph RBD/CephFS, NetApp Trident, Portworx, cloud disks (EBS, GCE PD, Azure Disk), local path.
- Object storage: S3 via Mountpoint for S3 or JuiceFS.
- Dataset caching: Fluid (Alluxio/JuiceFS), a Kubernetes dataset-cache project relevant to PR-5.
- Recommendation:
- Make XCE's own claim and provider contract CSI-shaped: the same verbs (create, publish, stage, node publish, capacity, snapshot, expand), capabilities, access modes, topology and per-call secrets.
- v1 ships simple native JSON providers (local scratch, NFS or parallel-filesystem directories).
- Add a CSI adapter later as one provider type in the proxy, plus a node helper for the worker.
- That keeps CSI a cheap "must later" rather than a v1 cost. If containers (R6) or cloud block storage become early targets, CSI moves up.
Resource model questions (from resource-model.md)
| ID | Question | Status | User input / notes |
|---|---|---|---|
| R1 | Phase 1 scope: shared pools only (option 2) with runtime values split out, or option 3 in one spec? | discussing | Reframed by PR-1: phase 1 may become the generic supply map. Decide after PR-1. |
| R2 | Who reports runtime custom values: worker scripts, a reporter role, or both? | leaning | Leaning both: worker scripts (elim-like, posix_spawn) for host values; the PR-2 proxy for external and cluster values. |
| R3 | Are claims first-class objects owned by a project from v1? Do they count against quotas? | deferred | User 2026-10-11: RBAC is handled in the API server; the backend only provides configuration and state persistence. Discuss later (with N4). |
| R4 | Own JSON driver contract, or CSI and device-plugin wire compatibility? | leaning | User 2026-10-11: CSI compatibility is better. Still to decide whether it is nice-to-have or a must in future (C3). Either way, model the operations on CSI so an adapter in the PR-2 proxy is mechanical. |
| R5 | Topology depth: host plus switch block, or per-GPU/NIC graphs? | leaning | User 2026-10-11: build GPU/topology into the scheduler first, with the capability to keep improving it. Means resource-instances delivers an extensible inventory model plus an allocator plugin interface, starting simple. |
| R6 | OCI containers as an execution mode? | leaning | User 2026-10-11: possible through the application profile (it already reserves a container mode). Profile defines image, runtime, mounts and device passthrough. |
| R7 | Who changes MIG layouts: a provider or an admin? | open | |
| R8 | Honour rusage duration/decay? |
decided | User 2026-10-11: must-have features, handled in a future spec (not the current phase). |
| R9 | Where does data staging live? | discussing | PR-3: shared datasets are provisioned copies in the map; job-private staging is a claim. |
Provisioner questions (from .kiro/specs/resource-provisioner/research.md)
| ID | Question | Status | Notes |
|---|---|---|---|
| P1 | Specify the persistent-provider JSON-lines format now (not in v1)? | open | Leaning yes, so v1 schemas carry over. |
| P2 | Is Go via CGO into libxce acceptable for the provisioner/proxy daemon? | open | Leaning yes (apiserver pattern; cloud SDKs native). |
| P3 | Must peer authentication (per-role opcode allowlist) ship before the provisioner? | open | Related to N4: component authentication is separate from user RBAC. |
| P4 | Fix the wire byte order and publish the protocol as stable? | open | |
| P5 | Are cloud accounts dedicated to the cluster, or shared? | open | If shared, tagged-but-unknown instances stay report-only. |
| P6 | Automatic return of never-joined instances in v1? | open | Leaning yes, after a join deadline. |
| P7 | Offer stop (quarantine) as well as terminate? |
open | |
| P8 | Settle window after failover: fixed time, or until expected workers rejoin? | open | |
| P9 | License server vs XCE in-use counts: which wins? | open |
New questions raised in discussion
| ID | Question | Status | Notes |
|---|---|---|---|
| N1 | Does the supply map (PR-1) also cover hosts provisioned by elastic-hosts, or only resources on hosts and pools? |
discussing | User 2026-10-11: hosts join the controller directly today; how could they be unified? Proposal PR-4 below. |
| N2 | One proxy (PR-2) for every provider kind, or one per kind (cloud, license, storage)? How does it map onto the provisioner's per-provider leases? | leaning | User 2026-10-11: there may be multiple proxies. Matches the provisioner brief: several instances, each provider owned by exactly one active proxy at a time through a controller lease, with standbys taking over. Still open: N2a, N2b. |
| N2a | How does a proxy declare which providers it serves: config pushed by the controller, or self-declared at join (and checked against config)? | open | |
| N2b | Can one proxy serve several kinds (cloud and storage), or one kind each? | open | Leaning: any mix; the lease is per provider, not per kind. |
| N3 | Dataset model (PR-3): identity and versions, read-only vs updatable, capacity accounting at each location, eviction policy, who triggers staging (scheduler demand, user, admin)? | discussing | User 2026-10-11: start with a simplified model. Proposal PR-5 below. |
| N4 | User RBAC in the apiserver vs component authorization in the controller (which daemon roles may call which opcodes): confirm both layers exist and where each check lives. | leaning | User 2026-10-11: XCE delegates access control to the API server, and the controller trusts it. Implication: the trust is only sound if the controller knows a request really comes from the apiserver (or another legitimate component). Today validate_host() accepts every peer (src/controller/join.c:13-16), so anything that reaches the controller port bypasses RBAC. Component authentication with a per-role opcode allowlist (peer-authentication-hardening, P3) is what makes the delegation safe, especially once proxies can spend money. |
| N6 | PR-6: with I/O claims, should -o/-e/-i and out_dir be accepted again even when no output claim is configured (e.g. a default per-job output directory claim), or stay rejected? |
open | |
| N7 | PR-6: where do uploaded inputs live between submit and job start (provider volume, apiserver staging area), and is there a size limit? | open | Large inputs should be datasets (PR-5). |
| N8 | PR-6: retention and quota defaults for output claims; who is charged (pool counter per project)? | discussing | With the shared volume: per-user or per-project quota on that volume (filesystem quota, or usage charged to the pool counter after the fact). Retention: see N14. |
| N9 | PR-6: multi-host jobs: a read-write-many output (shared filesystem), or per-host outputs collected after the job? | leaning | Read-write-many shared volume (PR-6 refinement). |
| N10 | PR-6: job arrays: one output claim with a subdirectory per element, or one claim per element? | leaning | Subdirectory per element: <jobid>/<index>/... (PR-6 refinement). |
| N11 | PR-6: requeue and restart: reuse the same output claim or create a new one? How does it relate to checkpoint claims? | open | |
| N12 | PR-6 shared volume: permissions and creation. <user> 0700 and user-owned under a root-owned top? Is the job directory created by the worker (as root, before dropping privileges) or by a provider? Path-safe encoding of array IDs with brackets? |
open | |
| N13 | PR-6 shared volume: a requeued job keeps its ID. Reuse <jobid>/ or add an attempt subdirectory (<jobid>/attempt-2)? (Refines N11.) |
open | |
| N14 | PR-6 shared volume: retention per job directory (TTL, after fetch), the cleaner (a provider task), and orphan directories whose job no longer exists. | open | |
| N15 | PR-6 shared volume: with several shared volumes, which one a job uses: by application profile, queue or project? | open | |
| C3 | CSI compatibility: nice-to-have or a must? If a must, which side: the controller plugin through the proxy, the node plugin on the worker, or both? | discussing | Background and recommendation in "CSI notes" below (2026-10-11). |
History
- 2026-10-11: user proposed a shared volume with per-user/per-job subdirectories (
shared_volume/<user>/<jobid>/{in,out,err}) instead of a volume per job → PR-6 refinement (leaning). N9 and N10 leaning; N8 reframed; N12–N15 added. - 2026-10-11: user asked whether volume provisioning can carry job input and output → PR-6; questions N6–N11 added.
- 2026-10-11: user input on N1 (hosts join directly; asked how to unify → PR-4), N2 (multiple proxies → leaning), N3 (a simplified model first → PR-5), N4 (access control delegated to the API server, the controller trusts it → leaning, with the component-authentication implication), C3 (asked for more on CSI → CSI notes). N2a and N2b added.
- 2026-10-11: created from the resource-model discussion. User input recorded for PR-1, PR-2 and PR-3 (proposals) and for R3, R4, R5, R6 and R8. Questions N1–N4 and C3 added.