Saltar al contenido
DispatchAtlas
Buscar

Contratos del dominio

dispatchatlas.core es el límite del paquete interno. Usa objetos inmutables y exclusivamente de la biblioteca estándar para que los benchmarks, los solvers, los experimentos, el análisis y la documentación consuman un único vocabulario de planificación compartido.

🗂️ Modelo de planificación

Los problemas se representan con ProblemSpec, TaskSpec, ResourceSpec, Dependency, Objective y Constraint. Las planificaciones candidatas usan Assignment, Schedule, ObjectiveValue y ScheduleResult.

La validación es explícita:

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 devuelve un ValidatedProblem con una marca de validación y un informe legible por máquina. validate_schedule comprueba la cobertura de tareas, la capacidad de recursos, los límites temporales y las dependencias.

🔗 Co-asignación multirrecurso

Una tarea puede demandar más de un recurso a la vez. TaskSpec.demands es una tupla de ResourceRequirements, y el constructor de la planificación co-asigna la tarea a cada recurso que demanda, manteniéndolos juntos durante toda la duración de la tarea. Por tanto, una planificación candidata registra cada ubicación como Assignment.resource_ids — una tupla, no un único recurso — y validate_schedule confirma que dos tareas que se solapan en el tiempo nunca comparten un recurso.

El grafo de conflicto de recursos es la palanca de planificación: dos tareas cuyos conjuntos de demanda se intersecan deben serializarse, mientras que dos cuyos conjuntos son disjuntos se ejecutan concurrentemente.

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)),
)

Las familias del continuo accelerator-coscheduling, distributed-transaction y fpga-partitioning ejercitan esta palanca — la primera con una retención fija de cómputo más acelerador, la segunda con un conjunto de bloqueos de cardinalidad variable sobre fragmentos de datos. El ejecutable examples/inspect_coallocation.py planifica ambas y muestra tareas de recursos disjuntos ejecutándose en paralelo mientras las tareas que comparten recursos se serializan.

🧩 Ejecución moldeable

Una tarea puede ejecutarse en cualquiera de varios modos. TaskSpec.modes es una tupla de TaskModes — cada uno un par (demands, duration), donde un modo más ancho (uno que demanda más recursos) se ejecuta en menos tiempo, la aceleración moldeable. Cuando modes está definido, la tarea es moldeable: el constructor de la planificación elige, por tarea, el modo que termina antes dados los recursos libres, de modo que el orden de la planificación decide cuánto paralelismo reclama cada tarea. Una tarea rígida deja modes sin definir (el valor por defecto) y su duration y demands de nivel superior son su único modo, que el backend exacto planifica como referencia conservadora. validate_schedule confirma que cada ubicación coincide con un modo declarado, y la ejecución moldeable y la imprecisa (mandatory_duration) son mutuamente excluyentes — una tarea elige su paralelismo o descarta trabajo opcional, no ambos.

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 familia del continuo elastic-serverless-autoscale ejercita esta palanca: cada invocación de función puede escalar a uno, dos o cuatro trabajadores tomados de un pequeño grupo de ráfaga compartido, de modo que el orden de la planificación decide qué invocaciones reclaman los escasos modos anchos-y-rápidos y cuáles se ejecutan estrechas — un compromiso entre latencia y coste de recursos.

🛰️ Co-planificación en pandilla (gang)

Las tareas que comparten un TaskSpec.gang_id forman una pandilla cuyos trabajadores deben arrancar todos al mismo tiempo en recursos distintos — un co-arranque de todo-o-nada, como necesita un trabajo síncrono de entrenamiento-distribuido o MPI con sus trabajadores ejecutándose juntos. El constructor en serie coloca una pandilla entera de forma atómica en el primer momento en que los recursos de cada trabajador están libres, incluso cuando un trabajador podría haber arrancado antes en solitario; validate_schedule rechaza una pandilla cuyos trabajadores no co-arrancan (schedule.feasibility.gang_cosched). Un trabajador de pandilla es rígido (no moldeable), ya que una pandilla co-arranca a un ancho fijo; una tarea independiente deja gang_id sin definir.

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 familia del continuo distributed-training-gang ejercita esta palanca: pandillas de dos a cuatro trabajadores reutilizan un grupo de aceleradores compartido y los trabajos llegan a lo largo del tiempo, de modo que un trabajo no puede empezar hasta que haya suficientes aceleradores libres simultáneamente y el orden de la planificación decide qué trabajo adquiere primero su conjunto completo de trabajadores.

⚖️ Reparto equitativo multiinquilino

Las tareas llevan un TaskSpec.tenant_id opcional que etiqueta al inquilino propietario para la contabilidad de reparto equitativo multiinquilino. El objetivo dominant-resource-share (ObjectiveKind.DOMINANT_RESOURCE_SHARE, según la equidad de recurso dominante de Ghodsi et al.) puntúa cuán uniformemente se equilibran las cuotas dominantes de los inquilinos: la cuota dominante de cada inquilino es la mayor fracción, entre recursos, de la capacidad-tiempo total de un recurso que ocupan sus tareas, y el objetivo es la dispersión entre el inquilino más y el menos atendido — 0.0 para una planificación de cuota-dominante-igual (equitativa), mayor cuando un inquilino monopoliza su recurso dominante. El objetivo es de sentido minimizar y devuelve DEFERRED en una instancia sin inquilinos. Un tenant_id es ortogonal a la ejecución en pandilla, moldeable e imprecisa — las tareas de un inquilino pueden ser cualquiera de ellas; una tarea sin inquilino deja tenant_id sin definir.

La palanca es la ubicación, no el orden de la planificación: la ocupación total de recursos de un inquilino está fijada por su carga de trabajo, así que sobre qué recursos caen sus tareas es lo que equilibra o sesga las cuotas dominantes. El ejecutable examples/multi_tenant_fairshare_study.py puntúa una ubicación equitativa (dispersa) frente a una monopolizadora de un inquilino pesado y uno ligero en un grupo de tres nodos, haciendo explícita la palanca.

⏱️ Plazos duros y blandos

Una tarea puede llevar un TaskSpec.deadline de finalización, y TaskSpec.deadline_kind clasifica cómo se juzga un incumplimiento. El valor por defecto ConstraintKind.HARD hace de un plazo incumplido una violación de factibilidad: validate_schedule plantea un problema bloqueante schedule.feasibility.deadline y la planificación se reporta infactible. ConstraintKind.SOFT hace del mismo incumplimiento solo una penalización por tardanza — la planificación sigue siendo factible mientras el rebasamiento se acumula al objetivo lateness (ObjectiveKind.LATENESS), de modo que una tarea de tiempo-real-blando se penaliza por tardanza sin volver infactible la planificación. El tipo no tiene efecto cuando deadline está sin definir.

Las dos lecturas de un plazo son independientes: un plazo duro acota la región factible mientras un plazo blando da forma a la superficie del objetivo, y una tarea puede usar cualquiera. El ejecutable examples/infeasibility_study.py recorre el caso de plazo duro de principio a fin — una instancia sobre-restringida reportada infactible, y el orden menos-infactible cuando no existe ninguna planificación factible.

💰 Modelos de coste

La planificación consciente del coste se describe mediante un CostModel, un artefacto inmutable separado, vinculado a un problema por su identificador. Mantener la capa de coste fuera de ProblemSpec permite que un problema y sus datos de coste versionen y serialicen independientemente. Un modelo de coste agrupa cinco contratos opcionales:

  • ExecutionTimeMatrix — tiempos de procesamiento de máquina-no-relacionada (p_ij) para la familia R||Cmax. La matriz es dispersa: un par (task, machine) ausente significa que la tarea no puede ejecutarse allí, y consultarlo plantea un error en lugar de devolver cero por defecto.
  • CompatibilityMask — un conjunto explícito de elegibilidad tarea-a-máquina, mantenido distinto de la matriz de ejecución para que la elegibilidad y el tiempo tabulado puedan divergir.
  • SetupMatrix — tiempos de preparación dependientes de la secuencia, indexados por transición tarea-a-tarea o tipo-a-tipo en una máquina.
  • LoadModel — una curva de ejecución dependiente de la carga que mapea un nivel de carga a un multiplicador de duración mediante interpolación lineal por tramos o por escalones entre puntos de quiebre estrictamente crecientes.
  • CommunicationModel — penalizaciones de comunicación entre recursos sobre un grafo disperso; la comunicación en la misma máquina es gratuita y los pares entre-máquinas no listados recurren a una penalización por defecto declarada.
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)]
    ),
)

🎯 Objetivos y reducciones

La familia de objetivos se nombra en OBJECTIVE_FAMILY, que registra el sentido y la unidad canónicos de cada objetivo: makespan, energía, coste, carbono, latencia, tardanza, equidad, cuota de recurso dominante (la dispersión entre la cuota de recurso dominante del inquilino más y el menos atendido — reparto equitativo multiinquilino), fiabilidad, seguridad, robustez, preparación, recompensa imprecisa (la computación opcional completada más allá de la parte obligatoria de tareas parcialmente ejecutables), y composiciones ponderadas. Un resultado medido lo lleva VectorObjectiveValue, que contiene una o más entradas ObjectiveValue (ordenadas por nombre) más una ObjectiveReduction explícita y una etiqueta de divulgación. Las reducciones de suma-ponderada y de Chebyshev escalarizan el vector; las reducciones no escalarizantes (NONE, LEXICOGRAPHIC) plantean un error en scalarize() para que las personas llamantes gestionen el ordenamiento explícitamente.

Informe de factibilidad

build_feasibility_report convierte un informe de validación de planificación en un FeasibilityReport visible para el revisor: las violaciones duras se convierten en registros InfeasibleRow por fila, y las restricciones blandas violadas con nombre acumulan entradas SoftConstraintPenalty ponderadas. Una violación blanda nunca voltea feasible a False.

📐 Evaluación de objetivos, restricciones y fronteras

La capa de objetivos-y-restricciones computa cada objetivo nombrado individualmente, de modo que ninguno se pliega en un paquete genérico. evaluate_objective deriva makespan, tardanza, equidad de carga (índice de Jain) y — para instancias etiquetadas por inquilino — equidad de cuota de recurso dominante directamente de una planificación, deriva el coste de una matriz de tiempo de ejecución adjunta y el tiempo de preparación dependiente de la secuencia de una matriz de preparación adjunta, y reduce los componentes computados en una composición ponderada. Los objetivos que necesitan un modelo que el núcleo no lleva (energía, carbono, latencia, fiabilidad, seguridad, robustez) devuelven una ObjectiveEvaluation con estado DEFERRED y una justificación nombrada en lugar de un valor fabricado. MultiObjectiveOutcome lleva los valores escalares, el objetivo vectorial y el informe de factibilidad a través de un viaje de ida y vuelta de serialización determinista.

La contabilidad de restricciones añade la restricción de nivel de servicio (SLA) nombrada como una restricción de primera clase con un camino de incumplimiento duro (una infactibilidad) y un camino de penalización blanda (una penalización ponderada proporcional al incumplimiento). Un ConstraintViolationSummary agrega violaciones duras, penalizaciones blandas, resultados de SLA y diagnósticos de reparación; reporta infactible ya sea por una violación de regla dura o por un incumplimiento de SLA duro.

Los ayudantes de Pareto extraen el frente no dominado y reportan los cuatro indicadores de calidad nombrados seleccionados según Riquelme, Von Lücken & Barán (2015): hipervolumen (el indicador primario), IGD+ (un indicador de convergencia débilmente Pareto-conforme), el epsilon-indicador aditivo, y la dispersión (el indicador de diversidad). Cada indicador es computable individualmente. frontier_data construye registros listos para la frontera con una bandera de no dominación para el análisis y el renderizado del portal.

La robustez se cuantifica como un objetivo medible en lugar de una etiqueta de postura: evaluate_robustness agrega el valor de un objetivo sobre un conjunto de perturbaciones declarado explícitamente, ya sea como el valor de peor caso o como el valor en riesgo condicional (CVaR) de la peor cola. Tanto el conjunto de perturbaciones como la agregación se registran en el resultado.

⚠️ Limitaciones

El núcleo del kernel define contratos de coste, objetivo y restricción, y la capa de objetivos-y-restricciones que los evalúa; no optimiza. Los objetivos que necesitan un modelo que el núcleo no lleva (energía, carbono, latencia, fiabilidad, seguridad y robustez) se reportan como diferidos con una justificación nombrada en lugar de estimarse. El LoadModel evalúa solo su propia curva declarada, y ObjectiveDefinition lleva metadatos en lugar de un evaluador.

🌱 Procedencia y semillas

Los artefactos generados llevan registros Provenance, ArtifactHash, SourceReference, EnvironmentStamp y SeedLineage opcional. Los flujos de semillas se derivan de coordenadas estables:

from dispatchatlas.core import derive_seed
 
seed = derive_seed(42, "benchmark.smoke", 0)

La misma semilla raíz, espacio de nombres e índice siempre producen la misma semilla derivada.

📦 Serialización

Los objetos del núcleo serializan a través de mapeos canónicos compatibles con JSON y envoltorios ArtifactEnvelope. Los envoltorios vinculan las cargas útiles a hashes de contenido y copian ese hash de vuelta a la procedencia.

🔌 Protocolos

Los paquetes externos dependen de protocolos definidos en el núcleo:

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

Estos protocolos mantienen las importaciones de paquetes dirigidas hacia dentro a la vez que permiten que los paquetes concretos de benchmark, solver, experimento y análisis se compongan más adelante.