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 theR||Cmaxfamily. 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:
BenchmarkProviderSolverExperimentRunnerResultRepositoryDisclosurePolicyAnalysisExporter
These protocols keep package imports directed inward while allowing concrete benchmark, solver, experiment, and analysis packages to compose later.