Domänenverträge
dispatchatlas.core ist die innere Paketgrenze. Es verwendet unveränderliche
Objekte ausschließlich aus der Standardbibliothek, damit Benchmarks, Solver,
Experimente, Analyse und Dokumentation ein einziges gemeinsames
Scheduling-Vokabular nutzen.
🗂️ Scheduling-Modell
Probleme werden mit ProblemSpec, TaskSpec, ResourceSpec, Dependency,
Objective und Constraint dargestellt. Kandidaten-Schedules verwenden
Assignment, Schedule, ObjectiveValue und ScheduleResult.
Die Validierung ist explizit:
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 gibt ein ValidatedProblem mit einem Validierungsstempel und
einem maschinenlesbaren Bericht zurück. validate_schedule prüft die
Aufgabenabdeckung, die Ressourcenkapazität, die Zeitschranken und die
Abhängigkeiten.
🔗 Mehrfachressourcen-Co-Allokation
Eine Aufgabe kann mehr als eine Ressource gleichzeitig anfordern. TaskSpec.demands
ist ein Tupel von ResourceRequirements, und der Schedule-Konstruktor co-alloziert
die Aufgabe über jede Ressource, die sie anfordert, und hält sie für die gesamte
Dauer der Aufgabe zusammen. Ein Kandidaten-Schedule erfasst daher jede Platzierung
als Assignment.resource_ids — ein Tupel, nicht eine einzelne Ressource — und
validate_schedule bestätigt, dass zwei zeitlich überlappende Aufgaben niemals eine
Ressource teilen.
Der Ressourcenkonflikt-Graph ist der Scheduling-Hebel: zwei Aufgaben, deren Anforderungsmengen sich schneiden, müssen serialisieren, während zwei mit disjunkten Mengen nebenläufig laufen.
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)),
)Die Kontinuum-Familien accelerator-coscheduling, distributed-transaction und fpga-partitioning
üben diesen Hebel — die erste mit einer festen Rechen-plus-Beschleuniger-Haltung,
die zweite mit einer Sperrmenge variabler Kardinalität über Datenfragmenten. Das
ausführbare
examples/inspect_coallocation.py
plant beide und zeigt Aufgaben mit disjunkten Ressourcen, die parallel laufen,
während ressourcenteilende Aufgaben serialisieren.
🧩 Formbare Ausführung
Eine Aufgabe kann in einem von mehreren Modi laufen. TaskSpec.modes ist ein Tupel
von TaskModes — jeder ein (demands, duration)-Paar, wobei ein breiterer Modus
(einer, der mehr Ressourcen anfordert) kürzer läuft, die formbare Beschleunigung.
Wenn modes gesetzt ist, ist die Aufgabe formbar: der Schedule-Konstruktor wählt
pro Aufgabe den Modus, der angesichts der freien Ressourcen am frühesten endet,
sodass die Schedule-Reihenfolge entscheidet, wie viel Parallelität jede Aufgabe
beansprucht. Eine starre Aufgabe lässt modes ungesetzt (der Standard), und ihre
duration und demands auf oberster Ebene sind ihr einziger Modus, den das exakte
Backend als konservative Referenz plant. validate_schedule bestätigt, dass jede
Platzierung einem deklarierten Modus entspricht, und formbare und unscharfe
(mandatory_duration) Ausführung schließen sich gegenseitig aus — eine Aufgabe wählt
ihre Parallelität oder verwirft optionale Arbeit, nicht beides.
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),
),
)Die Kontinuum-Familie elastic-serverless-autoscale übt diesen Hebel: jede
Funktionsaufrufung kann auf einen, zwei oder vier Worker aus einem kleinen
gemeinsamen Burst-Pool skalieren, sodass die Schedule-Reihenfolge entscheidet,
welche Aufrufungen die knappen breiten-und-schnellen Modi beanspruchen und welche
schmal laufen — ein Kompromiss zwischen Latenz und Ressourcenkosten.
🛰️ Gang-Co-Scheduling
Aufgaben, die eine TaskSpec.gang_id teilen, bilden eine Gang, deren Worker alle
zur gleichen Zeit auf verschiedenen Ressourcen starten müssen — ein
Alles-oder-Nichts-Co-Start, wie ein synchroner Distributed-Training- oder MPI-Job
seine Worker gemeinsam laufend braucht. Der serielle Konstruktor platziert eine
ganze Gang atomar zum frühesten Moment, in dem die Ressourcen jedes Workers frei
sind, selbst wenn ein Worker allein früher hätte starten können;
validate_schedule weist eine Gang zurück, deren Worker nicht co-starten
(schedule.feasibility.gang_cosched). Ein Gang-Worker ist starr (nicht formbar), da
eine Gang mit fester Breite co-startet; eine unabhängige Aufgabe lässt gang_id
ungesetzt.
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)
)Die Kontinuum-Familie distributed-training-gang übt diesen Hebel: Gangs von zwei
bis vier Workern verwenden einen gemeinsamen Beschleuniger-Pool wieder und Jobs
treffen über die Zeit ein, sodass ein Job nicht beginnen kann, bis genug
Beschleuniger gleichzeitig frei sind, und die Schedule-Reihenfolge entscheidet,
welcher Job zuerst seine vollständige Worker-Menge erwirbt.
⚖️ Mehrmandanten-Fair-Share
Aufgaben tragen eine optionale TaskSpec.tenant_id, die den besitzenden Mandanten
für die Mehrmandanten-Fair-Share-Buchhaltung kennzeichnet. Das Ziel
dominant-resource-share (ObjectiveKind.DOMINANT_RESOURCE_SHARE, nach der
Dominant-Resource-Fairness von Ghodsi et al.) bewertet, wie gleichmäßig die
dominanten Anteile der Mandanten ausbalanciert sind: der dominante Anteil jedes
Mandanten ist der größte Bruchteil, über die Ressourcen hinweg, der gesamten
Kapazität-Zeit einer Ressource, die seine Aufgaben belegen, und das Ziel ist die
Streuung zwischen dem meist- und dem am wenigsten bedienten Mandanten — 0.0 für
einen Schedule mit gleichem dominanten Anteil (fair), höher, wenn ein Mandant seine
dominante Ressource monopolisiert. Das Ziel ist minimierend und gibt auf einer
mandantenlosen Instanz DEFERRED zurück. Eine tenant_id ist orthogonal zur Gang-,
formbaren und unscharfen Ausführung — die Aufgaben eines Mandanten können jede davon
sein; eine mandantenlose Aufgabe lässt tenant_id ungesetzt.
Der Hebel ist die Platzierung, nicht die Schedule-Reihenfolge: die gesamte
Ressourcenbelegung eines Mandanten ist durch seine Arbeitslast fixiert, daher ist
es das, worauf seine Aufgaben landen, was die dominanten Anteile ausbalanciert oder
verzerrt. Das ausführbare
examples/multi_tenant_fairshare_study.py
bewertet eine faire (gestreute) gegenüber einer monopolisierenden Platzierung eines
schweren und eines leichten Mandanten auf einem Drei-Knoten-Pool und macht den Hebel
explizit.
⏱️ Harte und weiche Fristen
Eine Aufgabe kann eine Fertigstellungsfrist TaskSpec.deadline tragen, und
TaskSpec.deadline_kind klassifiziert, wie ein Versäumnis beurteilt wird. Der
Standard ConstraintKind.HARD macht eine versäumte Frist zu einer
Machbarkeitsverletzung: validate_schedule löst ein blockierendes Problem
schedule.feasibility.deadline aus und der Schedule meldet infeasible.
ConstraintKind.SOFT macht dasselbe Versäumnis nur zu einer Verspätungsstrafe — der
Schedule bleibt machbar, während der Überschuss dem lateness-Ziel
(ObjectiveKind.LATENESS) zufließt, sodass eine Soft-Echtzeit-Aufgabe für
Verspätung bestraft wird, ohne den Schedule infeasible zu machen. Der Typ hat keine
Wirkung, wenn deadline ungesetzt ist.
Die zwei Lesarten einer Frist sind unabhängig: eine harte Frist begrenzt den
machbaren Bereich, während eine weiche Frist die Zielfläche formt, und eine Aufgabe
kann beide nutzen. Das ausführbare
examples/infeasibility_study.py
durchläuft den Harte-Frist-Fall von Anfang bis Ende — eine überbeschränkte Instanz,
die infeasible gemeldet wird, und die am-wenigsten-infeasible Reihenfolge, wenn kein
machbarer Schedule existiert.
💰 Kostenmodelle
Kostenbewusstes Scheduling wird durch ein CostModel beschrieben, ein separates
unveränderliches Artefakt, das durch seinen Bezeichner mit einem Problem verknüpft
ist. Die Kostenschicht aus ProblemSpec herauszuhalten erlaubt es einem Problem und
seinen Kostendaten, unabhängig zu versionieren und zu serialisieren. Ein Kostenmodell
bündelt fünf optionale Verträge:
ExecutionTimeMatrix— Verarbeitungszeiten unverwandter Maschinen (p_ij) für dieR||Cmax-Familie. Die Matrix ist dünn besetzt: ein fehlendes(task, machine)-Paar bedeutet, dass die Aufgabe dort nicht laufen kann, und ihre Abfrage löst einen Fehler aus, statt auf null zu defaulten.CompatibilityMask— eine explizite Aufgabe-zu-Maschine-Eignungsmenge, getrennt von der Ausführungsmatrix gehalten, damit Eignung und tabellierte Zeit divergieren können.SetupMatrix— sequenzabhängige Rüstzeiten, indexiert nach Aufgabe-zu-Aufgabe- oder Typ-zu-Typ-Übergang auf einer Maschine.LoadModel— eine lastabhängige Ausführungskurve, die ein Lastniveau über stückweise-lineare oder Stufen-Interpolation zwischen streng steigenden Stützpunkten auf einen Dauer-Multiplikator abbildet.CommunicationModel— Kommunikationsstrafen zwischen Ressourcen auf einem dünn besetzten Graphen; Kommunikation auf derselben Maschine ist kostenlos und nicht gelistete Zwischen-Maschinen-Paare fallen auf eine deklarierte Standardstrafe zurück.
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)]
),
)🎯 Ziele und Reduktionen
Die Zielfamilie wird in OBJECTIVE_FAMILY benannt, das den kanonischen Sinn und die
Einheit jedes Ziels festhält: Makespan, Energie, Kosten, Kohlenstoff, Latenz,
Verspätung, Fairness, dominanter Ressourcenanteil (die Streuung zwischen dem
dominanten Ressourcenanteil des meist- und des am wenigsten bedienten Mandanten —
Mehrmandanten-Fair-Share), Zuverlässigkeit, Sicherheit, Robustheit, Rüstung,
unscharfe Belohnung (die optionale Berechnung, die über den verpflichtenden Teil
teilweise ausführbarer Aufgaben hinaus abgeschlossen wird) und gewichtete
Komposite. Ein gemessenes Ergebnis wird von VectorObjectiveValue getragen, das
einen oder mehrere ObjectiveValue-Einträge (nach Name geordnet) plus eine
explizite ObjectiveReduction und ein Offenlegungslabel enthält. Gewichtete-Summen-
und Chebyshev-Reduktionen skalarisieren den Vektor; die nicht-skalarisierenden
Reduktionen (NONE, LEXICOGRAPHIC) lösen bei scalarize() einen Fehler aus, damit
die Aufrufer die Ordnung explizit handhaben.
✅ Machbarkeitsbericht
build_feasibility_report verwandelt einen Schedule-Validierungsbericht in einen
prüferseitig sichtbaren FeasibilityReport: harte Verletzungen werden zu
InfeasibleRow-Datensätzen pro Zeile, und benannte verletzte weiche Constraints
sammeln gewichtete SoftConstraintPenalty-Einträge an. Eine weiche Verletzung kippt
feasible niemals auf False.
📐 Zielbewertung, Constraints und Frontier
Die Ziele-und-Constraints-Schicht berechnet jedes benannte Ziel individuell, sodass
keines in ein generisches Bündel gefaltet wird. evaluate_objective leitet
Makespan, Verspätung, Lastfairness (Jains Index) und — für mandantengekennzeichnete
Instanzen — dominanter-Ressourcenanteil-Fairness direkt aus einem Schedule ab,
leitet Kosten aus einer angehängten Ausführungszeitmatrix und sequenzabhängige
Rüstzeit aus einer angehängten Rüstmatrix ab und reduziert die berechneten
Komponenten zu einem gewichteten Komposit. Ziele, die ein Modell brauchen, das der
Kern nicht trägt (Energie, Kohlenstoff, Latenz, Zuverlässigkeit, Sicherheit,
Robustheit), geben eine ObjectiveEvaluation mit Status DEFERRED und einer
benannten Begründung statt eines fabrizierten Werts zurück. MultiObjectiveOutcome
trägt die Skalarwerte, das Vektorziel und den Machbarkeitsbericht durch einen
deterministischen Serialisierungs-Roundtrip.
Die Constraint-Buchhaltung fügt den benannten Service-Level-(SLA-)Constraint als
erstklassigen Constraint mit sowohl einem Hartbruch-Pfad (einer Infeasibilität) als
auch einem Weichstrafe-Pfad (einer gewichteten, dem Bruch proportionalen Strafe)
hinzu. Ein ConstraintViolationSummary aggregiert harte Verletzungen, weiche
Strafen, SLA-Ergebnisse und Reparaturdiagnosen; es meldet infeasible entweder bei
einer harten Regelverletzung oder bei einem harten SLA-Bruch.
Pareto-Helfer extrahieren die nicht-dominierte Front und melden die vier benannten
Qualitätsindikatoren, ausgewählt nach Riquelme, Von Lücken & Barán (2015):
Hypervolumen (der primäre Indikator), IGD+ (ein schwach-Pareto-konformer
Konvergenzindikator), der additive Epsilon-Indikator und die Streuung (der
Diversitätsindikator). Jeder Indikator ist individuell berechenbar. frontier_data
baut frontier-fertige Datensätze mit einem Nicht-dominiert-Flag für Analyse und
Portal-Rendering.
Robustheit wird als messbares Ziel statt als Haltungslabel quantifiziert:
evaluate_robustness aggregiert den Wert eines Ziels über eine explizit deklarierte
Perturbationsmenge, entweder als Worst-Case-Wert oder als bedingter Value-at-Risk
(CVaR) des schlechtesten Schwanzes. Sowohl die Perturbationsmenge als auch die
Aggregation werden auf dem Ergebnis festgehalten.
⚠️ Einschränkungen
Der Kern-Kernel definiert Kosten-, Ziel- und Constraint-Verträge sowie die
Ziele-und-Constraints-Schicht, die sie bewertet; er optimiert nicht. Ziele, die ein
Modell brauchen, das der Kern nicht trägt (Energie, Kohlenstoff, Latenz,
Zuverlässigkeit, Sicherheit und Robustheit), werden als zurückgestellt mit einer
benannten Begründung gemeldet statt geschätzt. Das LoadModel bewertet nur seine
eigene deklarierte Kurve, und ObjectiveDefinition trägt Metadaten statt eines
Evaluators.
🌱 Provenienz und Seeds
Generierte Artefakte tragen Provenance-, ArtifactHash-, SourceReference-,
EnvironmentStamp- und optionale SeedLineage-Datensätze. Seed-Streams werden aus
stabilen Koordinaten abgeleitet:
from dispatchatlas.core import derive_seed
seed = derive_seed(42, "benchmark.smoke", 0)Derselbe Wurzel-Seed, Namensraum und Index erzeugen immer denselben abgeleiteten Seed.
📦 Serialisierung
Kernobjekte serialisieren über kanonische JSON-kompatible Mappings und
ArtifactEnvelope-Wrapper. Envelopes binden Nutzlasten an Inhalts-Hashes und
kopieren diesen Hash zurück in die Provenienz.
🔌 Protokolle
Äußere Pakete hängen von kern-definierten Protokollen ab:
BenchmarkProviderSolverExperimentRunnerResultRepositoryDisclosurePolicyAnalysisExporter
Diese Protokolle halten die Paketimporte nach innen gerichtet und erlauben es zugleich konkreten Benchmark-, Solver-, Experiment- und Analysepaketen, sich später zu komponieren.