Skip to content
DispatchAtlas
Search

Domain Contracts

The dispatchatlas.core domain contracts — scheduling instances, machines, multi-resource tasks, objective families, validation reports, provenance, and deterministic serialization.

dispatchatlas.core is the inner package boundary. It uses immutable, standard-library-only objects so benchmarks, solvers, experiments, analysis, and documentation consume one shared scheduling vocabulary.

🗂️ Scheduling Model

Problems are represented with ProblemSpec, TaskSpec, ResourceSpec, Dependency, Objective, and Constraint. Candidate schedules use Assignment, Schedule, ObjectiveValue, and ScheduleResult.

Validation is explicit:

from dispatchatlas.core import (
    Duration,
    Objective,
    ObjectiveSense,
    ProblemId,
    ProblemSpec,
    ResourceAmount,
    ResourceId,
    ResourceRequirement,
    ResourceSpec,
    TaskId,
    TaskSpec,
    validate_problem,
)
 
cpu = ResourceId("cpu")
 
problem = ProblemSpec(
    id=ProblemId("smoke"),
    tasks=(
        TaskSpec(
            TaskId("task-a"),
            Duration(1.0),
            demands=(ResourceRequirement(cpu, ResourceAmount(1.0)),),
        ),
    ),
    resources=(ResourceSpec(cpu, ResourceAmount(1.0)),),
    objectives=(Objective("makespan", ObjectiveSense.MINIMIZE),),
)
 
validated = validate_problem(problem)

validate_problem returns a ValidatedProblem with a validation stamp and a machine-readable report. validate_schedule checks task coverage, resource capacity, timing bounds, and dependencies.

🔗 Multi-Resource Co-Allocation

A task may demand more than one resource at once. TaskSpec.demands is a tuple of ResourceRequirements, and the schedule constructor co-allocates the task across every resource it demands, holding them together for the task's whole duration. A candidate schedule therefore records each placement as Assignment.resource_ids — a tuple, not a single resource — and validate_schedule confirms no two time-overlapping tasks ever share a resource.

The resource-conflict graph is the scheduling lever: two tasks whose demand sets intersect must serialize, while two whose sets are disjoint run concurrently.

from dispatchatlas.core import ResourceAmount, ResourceId, ResourceRequirement
 
# A task that co-allocates a compute node and an accelerator simultaneously.
demands = (
    ResourceRequirement(ResourceId("edge-0"), ResourceAmount(1.0)),
    ResourceRequirement(ResourceId("gpu-1"), ResourceAmount(1.0)),
)

The accelerator-coscheduling, distributed-transaction, and fpga-partitioning continuum families exercise this lever — the first with a fixed compute-plus-accelerator hold, the second with a variable-cardinality lock set over data shards, the third with a spatially contiguous run of reconfigurable fabric tiles. The runnable examples/inspect_coallocation.py schedules the first two and shows disjoint-resource tasks running in parallel while resource-sharing tasks serialize.

🧩 Moldable Execution

A task may run in any one of several modes. TaskSpec.modes is a tuple of TaskModes — each a (demands, duration) pair, where a wider mode (one demanding more resources) runs shorter, the moldable speedup. When modes is set the task is moldable: the schedule constructor picks, per task, the mode that finishes earliest given which resources are free, so the schedule order decides how much parallelism each task claims. A rigid task leaves modes unset (the default) and its top-level duration and demands are its single mode, which the exact backend schedules as a conservative reference. validate_schedule confirms each placement matches one declared mode, and moldable and imprecise (mandatory_duration) execution are mutually exclusive — a task chooses its parallelism or drops optional work, not both.

from dispatchatlas.core import (
    Duration,
    ResourceAmount,
    ResourceId,
    ResourceRequirement,
    TaskMode,
)
 
# Two ways to run one job: wide-and-fast on two workers, or narrow-and-slow on one.
modes = (
    TaskMode(
        demands=(
            ResourceRequirement(ResourceId("worker-0"), ResourceAmount(1.0)),
            ResourceRequirement(ResourceId("worker-1"), ResourceAmount(1.0)),
        ),
        duration=Duration(1.0),
    ),
    TaskMode(
        demands=(ResourceRequirement(ResourceId("worker-0"), ResourceAmount(1.0)),),
        duration=Duration(1.8),
    ),
)

The elastic-serverless-autoscale continuum family exercises this lever: each function invocation may scale to one, two, or four workers drawn from a small shared burst pool, so the schedule order decides which invocations claim the scarce wide-and-fast modes and which run narrow — a latency-versus-resource-cost trade-off.

🛰️ Gang Co-Scheduling

Tasks sharing a TaskSpec.gang_id form a gang whose workers must all start at the same time on distinct resources — an all-or-nothing co-start, as a synchronous distributed-training or MPI job needs its workers running together. The serial constructor places a whole gang atomically at the earliest moment every worker's resources are free, even when one worker could have started earlier alone; validate_schedule rejects a gang whose workers do not co-start (schedule.feasibility.gang_cosched). A gang worker is rigid (not moldable), since a gang co-starts at a fixed width; an independent task leaves gang_id unset.

from dispatchatlas.core import (
    Duration,
    ResourceAmount,
    ResourceId,
    ResourceRequirement,
    TaskId,
    TaskSpec,
)
 
# Two workers of one training job that must launch together on distinct accelerators.
gang = "train-job-0"
workers = tuple(
    TaskSpec(
        id=TaskId(f"worker-{index}"),
        duration=Duration(2.0),
        demands=(ResourceRequirement(ResourceId(f"acc-{index}"), ResourceAmount(1.0)),),
        gang_id=gang,
    )
    for index in range(2)
)

The distributed-training-gang continuum family exercises this lever: gangs of two to four workers reuse a shared accelerator pool and jobs arrive over time, so a job cannot begin until enough accelerators are free simultaneously and the schedule order decides which job acquires its full worker set first.

⚖️ Multi-Tenant Fair-Share

Tasks carry an optional TaskSpec.tenant_id labelling their owning tenant for multi-tenant fair-share accounting. The dominant-resource-share objective (ObjectiveKind.DOMINANT_RESOURCE_SHARE, after Ghodsi et al.'s dominant resource fairness) scores how evenly tenants' dominant shares are balanced: each tenant's dominant share is the largest fraction, across resources, of a resource's total capacity-time its tasks occupy, and the objective is the spread between the most- and least-served tenant — 0.0 for an equal-dominant-share (fair) schedule, higher when one tenant monopolizes its dominant resource. The objective is minimize-sense and returns DEFERRED on an untenanted instance. A tenant_id is orthogonal to gang, moldable, and imprecise execution — a tenant's tasks may be any of them; an untenanted task leaves tenant_id unset.

The lever is placement, not schedule order: a tenant's total resource occupancy is fixed by its workload, so which resources its tasks land on is what balances or skews the dominant shares. The runnable examples/multi_tenant_fairshare_study.py scores a fair (spread) versus a monopolizing placement of a heavy and a light tenant on a three-node pool, making the lever explicit.

⏱️ Hard And Soft Deadlines

A task may carry a completion TaskSpec.deadline, and TaskSpec.deadline_kind classifies how a miss is judged. The default ConstraintKind.HARD makes a missed deadline a feasibility violation: validate_schedule raises a blocking schedule.feasibility.deadline issue and the schedule reports infeasible. ConstraintKind.SOFT makes the same miss a lateness penalty only — the schedule stays feasible while the overshoot accrues to the lateness objective (ObjectiveKind.LATENESS), so a soft-real-time task is penalized for tardiness without rendering the schedule infeasible. The kind has no effect when deadline is unset.

The two readings of a deadline are independent: a hard deadline bounds the feasible region while a soft deadline shapes the objective surface, and a task may use either. The runnable examples/infeasibility_study.py walks the hard-deadline case end to end — an over-constrained instance reported infeasible, and the least-infeasible order when no feasible schedule exists.

💰 Cost Models

Cost-aware scheduling is described by a CostModel, a separate immutable artifact linked to a problem by its identifier. Keeping the cost layer out of ProblemSpec lets a problem and its cost data version and serialize independently. A cost model bundles five optional contracts:

  • ExecutionTimeMatrix — unrelated-machine processing times (p_ij) for the R||Cmax family. The matrix is sparse: an absent (task, machine) pair means the task cannot run there, and querying it raises rather than defaulting to zero.
  • CompatibilityMask — an explicit task-to-machine eligibility set, kept distinct from the execution matrix so eligibility and tabulated time can diverge.
  • SetupMatrix — sequence-dependent setup times keyed by task-to-task or type-to-type transition on a machine.
  • LoadModel — a load-dependent execution curve mapping a load level to a duration multiplier through piecewise-linear or step interpolation between strictly increasing breakpoints.
  • CommunicationModel — cross-resource communication penalties on a sparse graph; same-machine communication is free and unlisted cross-machine pairs fall back to a declared default penalty.
from dispatchatlas.core import (
    CostModel,
    ProblemId,
    ResourceId,
    TaskId,
    execution_matrix_from_iterable,
)
 
cost_model = CostModel(
    problem_id=ProblemId("smoke"),
    execution_matrix=execution_matrix_from_iterable(
        [(TaskId("task-a"), ResourceId("cpu"), 1.0)]
    ),
)

🎯 Objectives And Reductions

The objective family is named in OBJECTIVE_FAMILY, which records the canonical sense and unit for each objective: makespan, energy, cost, carbon, latency, lateness, fairness, dominant resource share (the spread between the most- and least-served tenant's dominant resource share — multi-tenant fair-share), reliability, security, robustness, setup, imprecise reward (the optional computation completed beyond the mandatory part of partially- executable tasks), and weighted composites. A measured result is carried by VectorObjectiveValue, which holds one or more ObjectiveValue entries (ordered by name) plus an explicit ObjectiveReduction and a disclosure label. Weighted-sum and Chebyshev reductions scalarize the vector; the non-scalarizing reductions (NONE, LEXICOGRAPHIC) raise on scalarize() so callers handle ordering explicitly.

Feasibility Reporting

build_feasibility_report turns a schedule validation report into a reviewer-visible FeasibilityReport: hard violations become per-row InfeasibleRow records, and named violated soft constraints accrue weighted SoftConstraintPenalty entries. A soft violation never flips feasible to False.

📐 Objective Evaluation, Constraints, And Frontiers

The objectives-and-constraints layer computes each named objective individually, so none is folded into a generic bundle. evaluate_objective derives makespan, lateness, load fairness (Jain's index), and — for tenant-labelled instances — dominant-resource-share fairness directly from a schedule, derives cost from an attached execution-time matrix and sequence-dependent setup time from an attached setup matrix, and reduces the computed components into a weighted composite. Objectives that need a model the core does not carry (energy, carbon, latency, reliability, security, robustness) return an ObjectiveEvaluation with status DEFERRED and a named rationale rather than a fabricated value. MultiObjectiveOutcome carries the scalar values, the vector objective, and the feasibility report through a deterministic serialization round-trip.

Constraint accounting adds the named service-level (SLA) constraint as a first-class constraint with both a hard-breach path (an infeasibility) and a soft-penalty path (a weighted penalty proportional to the breach). A ConstraintViolationSummary aggregates hard violations, soft penalties, SLA outcomes, and repair diagnostics; it reports infeasible on either a hard rule violation or a hard SLA breach.

Pareto helpers extract the non-dominated front and report the four named quality indicators selected per Riquelme, Von Lücken & Barán (2015): hypervolume (the primary indicator), IGD+ (a weakly-Pareto-compliant convergence indicator), the additive epsilon-indicator, and spread (the diversity indicator). Each indicator is individually computable. frontier_data builds frontier-ready records with a non-dominated flag for analysis and portal rendering.

Robustness is quantified as a measurable objective rather than a posture label: evaluate_robustness aggregates one objective's value over an explicitly declared perturbation set, either as the worst-case value or as the conditional value-at-risk (CVaR) of the worst tail. Both the perturbation set and the aggregation are recorded on the result.

⚠️ Limitations

The core kernel defines cost, objective, and constraint contracts and the objectives-and-constraints layer that evaluates them; it does not optimize. Objectives that need a model the core does not carry (energy, carbon, latency, reliability, security, and robustness) are reported as deferred with a named rationale rather than estimated. The LoadModel evaluates only its own declared curve, and ObjectiveDefinition carries metadata rather than an evaluator.

🌱 Provenance And Seeds

Generated artifacts carry Provenance, ArtifactHash, SourceReference, EnvironmentStamp, and optional SeedLineage records. Seed streams are derived from stable coordinates:

from dispatchatlas.core import derive_seed
 
seed = derive_seed(42, "benchmark.smoke", 0)

The same root seed, namespace, and index always produce the same derived seed.

📦 Serialization

Core objects serialize through canonical JSON-compatible mappings and ArtifactEnvelope wrappers. Envelopes bind payloads to content hashes and copy that hash back into provenance.

🔌 Protocols

Outer packages depend on core-defined protocols:

  • BenchmarkProvider
  • Solver
  • ExperimentRunner
  • ResultRepository
  • DisclosurePolicy
  • AnalysisExporter

These protocols keep package imports directed inward while allowing concrete benchmark, solver, experiment, and analysis packages to compose later.