feat: define Slurm runtime and state record contracts - #878
Conversation
Define strict, versioned execution, readiness, output, reconciliation, and scheduler records. Enforce cross-record identity, digest, shard-set, and terminal-state invariants with deterministic golden fixtures and built-wheel coverage. Closes #865
Reject partial winners and conflicting shard ownership. Require explicit versions, enforce evidence chronology and readiness precedence, and cover every state-contract path with validated fixtures. Refs #865
Greptile SummaryThe PR establishes immutable, versioned Slurm records and deterministic serialization as the shared boundary between planning, execution, observation, retry, and collection.
|
| Filename | Overview |
|---|---|
| packages/data-designer-slurm/src/data_designer/slurm/contracts.py | Defines strict immutable contract primitives, path constraints, deterministic JSON serialization, and content digests. |
| packages/data-designer-slurm/src/data_designer/slurm/state/execution.py | Defines versioned run, shard, and attempt records with lifecycle consistency constraints. |
| packages/data-designer-slurm/src/data_designer/slurm/state/outputs.py | Defines candidate-output, winner, and collection records with record-count and uniqueness invariants. |
| packages/data-designer-slurm/src/data_designer/slurm/state/readiness.py | Defines deployment and attempt readiness snapshots with aggregate-state consistency checks. |
| packages/data-designer-slurm/src/data_designer/slurm/state/reconciliation.py | Implements readiness transition validation and deterministic precedence for attempt observations. |
| packages/data-designer-slurm/src/data_designer/slurm/state/validation.py | Implements cross-record identity, chronology, ownership, winner-publication, and collection validation. |
| packages/data-designer-slurm/pyproject.toml | Declares Pydantic as a direct dependency of the optional Slurm distribution. |
| scripts/test_slurm_package_install.py | Extends isolated-wheel verification to cover the packaged state API and dependency metadata. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
Run[Run manifest] --> Shards[Shard manifests]
Shards --> Attempts[Attempt manifests]
Attempts --> Scheduler[Scheduler observations]
Attempts --> Readiness[Readiness snapshots]
Scheduler --> Reconcile[Observation reconciliation]
Readiness --> Reconcile
Attempts --> Candidates[Candidate outputs]
Candidates --> Winners[Shard winners]
Winners --> Collection[Collection plan]
Reviews (3): Last reviewed commit: "refactor(slurm): share contract primitiv..." | Re-trigger Greptile
| shard_indices == tuple(range(run.shard_count)), | ||
| "shards must be ordered by a complete zero-based shard index", | ||
| ) | ||
| for previous, current in zip(shards, shards[1:]): |
There was a problem hiding this comment.
This accepts [0, 100) and [200, 300) as an exact two-shard set. Because RunManifest has no total-record bound and this validator never consults #873's resolved shard intent, all winners can validate while collection omits the missing range. Can we add the total record count to the run contract and require the ranges to form a contiguous [0, total_records) partition, with a gapped-range test?
There was a problem hiding this comment.
There is a cleaner answer to this now than the extra field I originally suggested. The resolved plan owns the requested total and complete ordered shard set, so RunManifest can stay minimal and the integration validator can compare every state shard directly with its planned shard. How does that ownership split sound?
There was a problem hiding this comment.
Agreed on the ownership split. The sequence is: #878 lands the canonical ShardId, RecordRange, ResumeWorkspace, and state-local checks; #879 then rebases, imports those exact public types, and removes its duplicate definitions; after that, a plan-aware integration validator compares the ordered state shards directly with ResolvedPlan.shards, including identities, indices, ranges, input partitions, and workspace paths. I am intentionally not duplicating the requested total in RunManifest. I am leaving this thread open until that integration check lands.
| revision: PositiveInt | ||
| updated_at: datetime | ||
| state: ReadinessState | ||
| deployments: tuple[DeploymentReadiness, ...] = Field(min_length=1) |
There was a problem hiding this comment.
When #873 adds the resolved deployment contract, could we add a plan-aware initial-snapshot validator that checks this tuple's identity, order, and expected backend counts? Transition validation preserves these values after the first snapshot, but nothing currently anchors the initial snapshot to authored or resolved intent.
There was a problem hiding this comment.
The resolved deployment shape now has a stable deployment_id, the unrestricted authored model_alias, and topology.replica_count, all in deployment order. Those seem like the right fields for anchoring the initial readiness snapshot, while the existing transition checks preserve them afterward. Let me know if that lines up with how you intended these entries to identify deployments.
There was a problem hiding this comment.
Yes, those are the intended anchor fields. Following the same sequence, #878 lands the canonical state and shared primitives first, then #879 rebases and reuses those exact classes. Once both contracts are on the same base, the initial-snapshot validator should compare deployment order, deployment_id, the unrestricted model_alias, and topology.replica_count; the existing transition validator preserves them afterward. I am leaving this open for that post-rebase integration.
andreatnvidia
left a comment
There was a problem hiding this comment.
Thanks, @nabinchha. The state boundary is well scoped. I found two contract issues that need to be fixed before the schema is frozen: referenced record digests do not match persisted file bytes, and exact shard sets can omit record ranges because the run contract has no total. I also left a non-blocking #873 integration note for anchoring the initial readiness snapshot to the resolved deployment list.
| return shards | ||
|
|
||
|
|
||
| def validate_attempt_manifest( |
There was a problem hiding this comment.
I think we need attempt-set and transition validation alongside this single-manifest check. Two shards can currently claim the same (array_job_id, array_task_id), and a later manifest can change its scheduler identity or move from SUCCEEDED back to RUNNING. Adding scheduler-identity uniqueness and terminal-immutability checks would preserve the contracts required by #865.
There was a problem hiding this comment.
Each planned shard now persists its array_task_index, along with the shard-NNNNN identity. When we connect the contracts, I think each attempt should match that task index and use attempt-NNNN derived from its ordinal. That would also catch mappings where task IDs are unique but assigned to the wrong shards.
There was a problem hiding this comment.
The state-owned portion is fixed in 7a4b1e0: validate_attempt_set enforces unique attempt identities, ordinals, and scheduler identities, while validate_attempt_transition preserves scheduler and candidate identity, enforces monotonic lifecycle, and makes terminal records immutable. The plan-owned mapping follows the agreed sequence: #878 first, #879 rebase and shared-type adoption second, then the integration validator checks array_task_index and the ordinal-derived attempt ID against the planned shard. I am leaving this open until that join lands.
|
A few non-blocking follow-ups seem worth folding in while these contracts are fresh:
|
Hash persisted record bytes, align state identities with the resolved plan vocabulary, and preserve public model aliases. Add monotonic attempt and scheduler transition checks, including fixed accounting-lag deadlines and UNKNOWN precedence. Refs #865
Make planning and runtime state consume one public immutable model family. Align shard, attempt, range, artifact, and resume workspace shapes so the configuration-plan branch can rebase without preserving duplicate Pydantic types.\n\nRefs #865
|
Thanks — the standalone follow-ups are addressed in 7a4b1e0 and 2ecc3f7:
For the cross-PR work, the intended sequence is:
I left those four integration threads open until step 3 lands. Local validation is green: Slurm lint, 90 tests, 100% contract/state coverage, and isolated wheel installation. The four plan-aware joins and their tests are tracked in #880: #880 |
andreatnvidia
left a comment
There was a problem hiding this comment.
Thanks for working through the feedback. With the remaining integration covered by #880, this looks good to me.
📋 Summary
Defines strict, versioned records shared across Slurm submission, allocation-local execution, fresh-process observation, retry, and collection. This establishes the deterministic serialization and validation boundary required by later Slurm implementation work without coupling consumers to scheduler commands or runtime services.
🔗 Related Issue
Closes #865
Related to #850. Builds on #851, #852, and #853.
🔄 Changes
✨ Added
🔧 Changed
🐛 Fixed
🔍 Attention Areas
🧪 Testing
make check-slurm.venv/bin/ruff format --check scripts/test_slurm_package_install.py.venv/bin/ruff check scripts/test_slurm_package_install.pymake test-slurm(80 passed)510/510statements)make test-slurm-wheel-install(0.456 s base median, 0.461 s extension median, 0.005 s overhead)make test— not run; validation was scoped to the optional Slurm package and its isolated wheel boundary.✅ Checklist