Contrats du domaine
dispatchatlas.core est la frontière du paquet interne. Il utilise des objets
immuables et issus uniquement de la bibliothèque standard afin que les benchmarks,
les solveurs, les expériences, l'analyse et la documentation consomment un seul
vocabulaire d'ordonnancement partagé.
🗂️ Modèle d'ordonnancement
Les problèmes sont représentés avec ProblemSpec, TaskSpec, ResourceSpec,
Dependency, Objective et Constraint. Les ordonnancements candidats utilisent
Assignment, Schedule, ObjectiveValue et ScheduleResult.
La validation est explicite :
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 renvoie un ValidatedProblem avec une estampille de validation
et un rapport lisible par machine. validate_schedule vérifie la couverture des
tâches, la capacité des ressources, les bornes temporelles et les dépendances.
🔗 Co-allocation multiressource
Une tâche peut demander plus d'une ressource à la fois. TaskSpec.demands est un
tuple de ResourceRequirements, et le constructeur de l'ordonnancement co-alloue la
tâche sur chaque ressource qu'elle demande, les maintenant ensemble pendant
toute la durée de la tâche. Un ordonnancement candidat enregistre donc chaque
placement comme Assignment.resource_ids — un tuple, pas une seule ressource — et
validate_schedule confirme que deux tâches qui se chevauchent dans le temps ne
partagent jamais une ressource.
Le graphe de conflit de ressources est le levier d'ordonnancement : deux tâches dont les ensembles de demande s'intersectent doivent se sérialiser, tandis que deux dont les ensembles sont disjoints s'exécutent simultanément.
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)),
)Les familles du continuum accelerator-coscheduling, distributed-transaction et fpga-partitioning
exercent ce levier — la première avec une rétention fixe de calcul plus
accélérateur, la seconde avec un ensemble de verrous de cardinalité variable sur des
fragments de données. L'exécutable
examples/inspect_coallocation.py
ordonnance les deux et montre des tâches à ressources disjointes s'exécutant en
parallèle tandis que les tâches partageant des ressources se sérialisent.
🧩 Exécution malléable
Une tâche peut s'exécuter dans l'un de plusieurs modes. TaskSpec.modes est un
tuple de TaskModes — chacun une paire (demands, duration), où un mode plus large
(qui demande plus de ressources) s'exécute plus vite, l'accélération malléable.
Lorsque modes est défini, la tâche est malléable : le constructeur de
l'ordonnancement choisit, par tâche, le mode qui se termine le plus tôt selon les
ressources libres, de sorte que l'ordre de l'ordonnancement décide combien de
parallélisme chaque tâche réclame. Une tâche rigide laisse modes non défini (la
valeur par défaut) et ses duration et demands de niveau supérieur sont son mode
unique, que le backend exact ordonnance comme référence conservatrice.
validate_schedule confirme que chaque placement correspond à un mode déclaré, et
l'exécution malléable et l'exécution imprécise (mandatory_duration) sont
mutuellement exclusives — une tâche choisit son parallelisme ou abandonne du travail
optionnel, pas les deux.
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),
),
)La famille du continuum elastic-serverless-autoscale exerce ce levier : chaque
invocation de fonction peut passer à un, deux ou quatre travailleurs tirés d'un petit
pool de rafale partagé, de sorte que l'ordre de l'ordonnancement décide quelles
invocations réclament les rares modes larges-et-rapides et lesquelles s'exécutent
étroites — un compromis entre latence et coût des ressources.
🛰️ Co-ordonnancement en gang
Les tâches partageant un TaskSpec.gang_id forment un gang dont les travailleurs
doivent tous démarrer en même temps sur des ressources distinctes — un co-démarrage
de tout-ou-rien, comme un travail synchrone d'entraînement-distribué ou MPI a besoin
de ses travailleurs s'exécutant ensemble. Le constructeur en série place un gang
entier de façon atomique au premier moment où les ressources de chaque travailleur
sont libres, même quand un travailleur aurait pu démarrer plus tôt seul ;
validate_schedule rejette un gang dont les travailleurs ne co-démarrent pas
(schedule.feasibility.gang_cosched). Un travailleur de gang est rigide (non
malléable), puisqu'un gang co-démarre à une largeur fixe ; une tâche indépendante
laisse gang_id non défini.
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)
)La famille du continuum distributed-training-gang exerce ce levier : des gangs de
deux à quatre travailleurs réutilisent un pool d'accélérateurs partagé et les travaux
arrivent au fil du temps, de sorte qu'un travail ne peut commencer tant qu'assez
d'accélérateurs ne sont pas libres simultanément et l'ordre de l'ordonnancement
décide quel travail acquiert son ensemble complet de travailleurs en premier.
⚖️ Partage équitable multilocataire
Les tâches portent un TaskSpec.tenant_id optionnel étiquetant le locataire
propriétaire pour la comptabilité de partage équitable multilocataire. L'objectif
dominant-resource-share (ObjectiveKind.DOMINANT_RESOURCE_SHARE, d'après l'équité
de ressource dominante de Ghodsi et al.) évalue à quel point les parts dominantes des
locataires sont équilibrées : la part dominante de chaque locataire est la plus
grande fraction, parmi les ressources, de la capacité-temps totale d'une ressource
que ses tâches occupent, et l'objectif est l'écart entre le locataire le plus et le
moins servi — 0.0 pour un ordonnancement à part-dominante-égale (équitable), plus
élevé quand un locataire monopolise sa ressource dominante. L'objectif est de sens
minimiser et renvoie DEFERRED sur une instance sans locataire. Un tenant_id est
orthogonal à l'exécution en gang, malléable et imprécise — les tâches d'un locataire
peuvent être l'une quelconque d'entre elles ; une tâche sans locataire laisse
tenant_id non défini.
Le levier est le placement, pas l'ordre de l'ordonnancement : l'occupation totale de
ressources d'un locataire est fixée par sa charge de travail, donc sur quelles
ressources atterrissent ses tâches est ce qui équilibre ou fausse les parts
dominantes. L'exécutable
examples/multi_tenant_fairshare_study.py
évalue un placement équitable (dispersé) face à un placement monopolisateur d'un
locataire lourd et d'un léger sur un pool de trois nœuds, rendant le levier
explicite.
⏱️ Échéances strictes et souples
Une tâche peut porter une échéance d'achèvement TaskSpec.deadline, et
TaskSpec.deadline_kind classe comment un manquement est jugé. La valeur par défaut
ConstraintKind.HARD fait d'une échéance manquée une violation de faisabilité :
validate_schedule soulève un problème bloquant schedule.feasibility.deadline et
l'ordonnancement se rapporte infaisable. ConstraintKind.SOFT fait du même
manquement seulement une pénalité de retard — l'ordonnancement reste faisable tandis
que le dépassement s'accumule à l'objectif lateness (ObjectiveKind.LATENESS), de
sorte qu'une tâche temps-réel-souple est pénalisée pour retard sans rendre
l'ordonnancement infaisable. Le type n'a aucun effet quand deadline est non défini.
Les deux lectures d'une échéance sont indépendantes : une échéance stricte borne la
région faisable tandis qu'une échéance souple façonne la surface de l'objectif, et
une tâche peut utiliser l'une ou l'autre. L'exécutable
examples/infeasibility_study.py
parcourt le cas de l'échéance stricte de bout en bout — une instance sur-contrainte
rapportée infaisable, et l'ordre le-moins-infaisable quand aucun ordonnancement
faisable n'existe.
💰 Modèles de coût
L'ordonnancement conscient du coût est décrit par un CostModel, un artefact
immuable séparé, lié à un problème par son identifiant. Garder la couche de coût hors
de ProblemSpec permet à un problème et à ses données de coût de versionner et
sérialiser indépendamment. Un modèle de coût regroupe cinq contrats optionnels :
ExecutionTimeMatrix— temps de traitement de machine-non-reliée (p_ij) pour la familleR||Cmax. La matrice est creuse : une paire(task, machine)absente signifie que la tâche ne peut s'y exécuter, et l'interroger soulève une erreur au lieu de défaut à zéro.CompatibilityMask— un ensemble explicite d'éligibilité tâche-à-machine, gardé distinct de la matrice d'exécution afin que l'éligibilité et le temps tabulé puissent diverger.SetupMatrix— temps de préparation dépendants de la séquence, indexés par transition tâche-à-tâche ou type-à-type sur une machine.LoadModel— une courbe d'exécution dépendante de la charge mappant un niveau de charge à un multiplicateur de durée par interpolation linéaire par morceaux ou par paliers entre des points de rupture strictement croissants.CommunicationModel— pénalités de communication entre ressources sur un graphe creux ; la communication sur la même machine est gratuite et les paires entre-machines non listées retombent sur une pénalité par défaut déclarée.
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)]
),
)🎯 Objectifs et réductions
La famille d'objectifs est nommée dans OBJECTIVE_FAMILY, qui enregistre le sens et
l'unité canoniques de chaque objectif : makespan, énergie, coût, carbone, latence,
retard, équité, part de ressource dominante (l'écart entre la part de ressource
dominante du locataire le plus et le moins servi — partage équitable multilocataire),
fiabilité, sécurité, robustesse, préparation, récompense imprécise (le calcul
optionnel achevé au-delà de la partie obligatoire des tâches partiellement
exécutables), et composites pondérés. Un résultat mesuré est porté par
VectorObjectiveValue, qui contient une ou plusieurs entrées ObjectiveValue
(ordonnées par nom) plus une ObjectiveReduction explicite et une étiquette de
divulgation. Les réductions de somme-pondérée et de Chebyshev scalarisent le vecteur ;
les réductions non scalarisantes (NONE, LEXICOGRAPHIC) soulèvent une erreur sur
scalarize() pour que les appelants gèrent l'ordonnancement explicitement.
✅ Rapport de faisabilité
build_feasibility_report transforme un rapport de validation d'ordonnancement en un
FeasibilityReport visible par le relecteur : les violations strictes deviennent des
enregistrements InfeasibleRow par ligne, et les contraintes souples violées nommées
accumulent des entrées SoftConstraintPenalty pondérées. Une violation souple ne
bascule jamais feasible à False.
📐 Évaluation des objectifs, contraintes et frontières
La couche objectifs-et-contraintes calcule chaque objectif nommé individuellement,
de sorte qu'aucun n'est plié dans un paquet générique. evaluate_objective dérive le
makespan, le retard, l'équité de charge (indice de Jain) et — pour les instances
étiquetées par locataire — l'équité de part de ressource dominante directement d'un
ordonnancement, dérive le coût d'une matrice de temps d'exécution attachée et le
temps de préparation dépendant de la séquence d'une matrice de préparation attachée,
et réduit les composantes calculées en un composite pondéré. Les objectifs qui ont
besoin d'un modèle que le cœur ne porte pas (énergie, carbone, latence, fiabilité,
sécurité, robustesse) renvoient une ObjectiveEvaluation avec le statut DEFERRED
et une justification nommée plutôt qu'une valeur fabriquée. MultiObjectiveOutcome
porte les valeurs scalaires, l'objectif vectoriel et le rapport de faisabilité à
travers un aller-retour de sérialisation déterministe.
La comptabilité des contraintes ajoute la contrainte de niveau de service (SLA)
nommée comme une contrainte de première classe avec à la fois un chemin de
manquement strict (une infaisabilité) et un chemin de pénalité souple (une pénalité
pondérée proportionnelle au manquement). Un ConstraintViolationSummary agrège les
violations strictes, les pénalités souples, les résultats de SLA et les diagnostics
de réparation ; il rapporte infaisable soit sur une violation de règle stricte, soit
sur un manquement de SLA strict.
Les utilitaires de Pareto extraient le front non dominé et rapportent les quatre
indicateurs de qualité nommés sélectionnés d'après Riquelme, Von Lücken & Barán
(2015) : hypervolume (l'indicateur primaire), IGD+ (un indicateur de convergence
faiblement Pareto-conforme), l'epsilon-indicateur additif, et la dispersion
(l'indicateur de diversité). Chaque indicateur est calculable individuellement.
frontier_data construit des enregistrements prêts pour la frontière avec un drapeau
non-dominé pour l'analyse et le rendu du portail.
La robustesse est quantifiée comme un objectif mesurable plutôt qu'une étiquette de
posture : evaluate_robustness agrège la valeur d'un objectif sur un ensemble de
perturbations déclaré explicitement, soit comme la valeur de pire cas, soit comme la
valeur à risque conditionnelle (CVaR) de la pire queue. L'ensemble de perturbations
et l'agrégation sont tous deux enregistrés sur le résultat.
⚠️ Limitations
Le noyau du kernel définit des contrats de coût, d'objectif et de contrainte, ainsi
que la couche objectifs-et-contraintes qui les évalue ; il n'optimise pas. Les
objectifs qui ont besoin d'un modèle que le cœur ne porte pas (énergie, carbone,
latence, fiabilité, sécurité et robustesse) sont rapportés comme différés avec une
justification nommée plutôt qu'estimés. Le LoadModel n'évalue que sa propre courbe
déclarée, et ObjectiveDefinition porte des métadonnées plutôt qu'un évaluateur.
🌱 Provenance et graines
Les artefacts générés portent des enregistrements Provenance, ArtifactHash,
SourceReference, EnvironmentStamp et SeedLineage optionnel. Les flux de graines
sont dérivés de coordonnées stables :
from dispatchatlas.core import derive_seed
seed = derive_seed(42, "benchmark.smoke", 0)La même graine racine, le même espace de noms et le même indice produisent toujours la même graine dérivée.
📦 Sérialisation
Les objets du cœur sérialisent à travers des mappages canoniques compatibles JSON et
des enveloppes ArtifactEnvelope. Les enveloppes lient les charges utiles à des
hachages de contenu et recopient ce hachage dans la provenance.
🔌 Protocoles
Les paquets externes dépendent de protocoles définis dans le cœur :
BenchmarkProviderSolverExperimentRunnerResultRepositoryDisclosurePolicyAnalysisExporter
Ces protocoles maintiennent les imports de paquets dirigés vers l'intérieur tout en permettant aux paquets concrets de benchmark, de solveur, d'expérience et d'analyse de se composer plus tard.