199 lines
22 KiB
Markdown
199 lines
22 KiB
Markdown
# 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-mapping` be 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-provisioner` daemon. 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).
|
||
- **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-hosts` plans "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 static `hosts.yaml` entry 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`, plus `lost`.
|
||
- **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`, `mem` and so on stay L2 counters at the host's location, as today.
|
||
- **Bring-up:** `xce-node-manager` can 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-demand` sees 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@version` and 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 → evicting` and 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 reports `available`. Eviction is explicit too.
|
||
- **Later:** scheduler-driven staging (shortfall as provisioning demand), automatic LRU eviction, dataset quotas, and writable datasets.
|
||
- **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_dir` and `transfer_files` at 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).
|
||
- **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/-e` and `out_dir` resolve 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 `mkdir` with 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.
|
||
- **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 `mkdir` plus 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 `subPath` and 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`.
|
||
- **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=a` or `rack=r1`; `CreateVolume` takes 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:** `CreateVolume` when a claim or dataset copy is created; `ControllerPublish` when it is bound to hosts; `Delete` on release; `GetCapacity` → pool counters in the map; snapshots → checkpoint snapshots; secrets stay in the proxy.
|
||
- **Node side → worker/xcecd:** `NodeStage` on first use on a host; `NodePublish` into the job's sandbox (or container) path; unpublish and unstage at job end. `NodeGetInfo` topology → 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.
|
||
- **Costs:**
|
||
- gRPC clients: natural in Go (the proxy; P2), awkward in C (the worker). A small Go node helper, started with `posix_spawn` or run under `xce-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.
|
||
- **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.
|