Zum Inhalt springen
DispatchAtlas
Suchen

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 die R||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:

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

Diese Protokolle halten die Paketimporte nach innen gerichtet und erlauben es zugleich konkreten Benchmark-, Solver-, Experiment- und Analysepaketen, sich später zu komponieren.