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 familiaR||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:
BenchmarkProviderSolverExperimentRunnerResultRepositoryDisclosurePolicyAnalysisExporter
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.