Aller au contenu
DispatchAtlas
Rechercher

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

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

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.