Contratos do domínio
dispatchatlas.core é a fronteira do pacote interno. Ele usa objetos imutáveis e
exclusivamente da biblioteca padrão para que benchmarks, solvers, experimentos,
análise e documentação consumam um único vocabulário de escalonamento compartilhado.
🗂️ Modelo de escalonamento
Os problemas são representados com ProblemSpec, TaskSpec, ResourceSpec,
Dependency, Objective e Constraint. Os escalonamentos candidatos usam
Assignment, Schedule, ObjectiveValue e ScheduleResult.
A validação é 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 retorna um ValidatedProblem com um carimbo de validação e um
relatório legível por máquina. validate_schedule verifica a cobertura de tarefas,
a capacidade de recursos, os limites de tempo e as dependências.
🔗 Coalocação multirrecurso
Uma tarefa pode demandar mais de um recurso ao mesmo tempo. TaskSpec.demands é uma
tupla de ResourceRequirements, e o construtor do escalonamento coaloca a tarefa em
cada recurso que ela demanda, mantendo-os juntos por toda a duração da tarefa.
Um escalonamento candidato, portanto, registra cada posicionamento como
Assignment.resource_ids — uma tupla, não um único recurso — e validate_schedule
confirma que duas tarefas que se sobrepõem no tempo nunca compartilham um recurso.
O grafo de conflito de recursos é a alavanca de escalonamento: duas tarefas cujos conjuntos de demanda se interceptam devem serializar, enquanto duas cujos conjuntos são disjuntos rodam concorrentemente.
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)),
)As famílias do contínuo accelerator-coscheduling, distributed-transaction e fpga-partitioning
exercitam essa alavanca — a primeira com uma retenção fixa de computação mais
acelerador, a segunda com um conjunto de bloqueios de cardinalidade variável sobre
fragmentos de dados. O executável
examples/inspect_coallocation.py
escalona ambas e mostra tarefas de recursos disjuntos rodando em paralelo enquanto
tarefas que compartilham recursos serializam.
🧩 Execução moldável
Uma tarefa pode rodar em qualquer um de vários modos. TaskSpec.modes é uma tupla
de TaskModes — cada um um par (demands, duration), em que um modo mais largo (um
que demanda mais recursos) roda em menos tempo, a aceleração moldável. Quando modes
está definido, a tarefa é moldável: o construtor do escalonamento escolhe, por
tarefa, o modo que termina mais cedo dados os recursos livres, de modo que a ordem do
escalonamento decide quanto paralelismo cada tarefa reivindica. Uma tarefa rígida
deixa modes indefinido (o padrão) e seus duration e demands de nível superior
são seu único modo, que o backend exato escalona como referência conservadora.
validate_schedule confirma que cada posicionamento corresponde a um modo declarado,
e a execução moldável e a imprecisa (mandatory_duration) são mutuamente exclusivas
— uma tarefa escolhe seu paralelismo ou descarta trabalho opcional, não 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),
),
)A família do contínuo elastic-serverless-autoscale exercita essa alavanca: cada
invocação de função pode escalar para um, dois ou quatro trabalhadores retirados de
um pequeno pool de rajada compartilhado, de modo que a ordem do escalonamento decide
quais invocações reivindicam os escassos modos largos-e-rápidos e quais rodam
estreitas — um compromisso entre latência e custo de recursos.
🛰️ Coescalonamento em gangue
Tarefas que compartilham um TaskSpec.gang_id formam uma gangue cujos
trabalhadores devem todos iniciar ao mesmo tempo em recursos distintos — um
coinício de tudo-ou-nada, como um trabalho síncrono de treinamento-distribuído ou MPI
precisa de seus trabalhadores rodando juntos. O construtor serial posiciona uma
gangue inteira de forma atômica no primeiro momento em que os recursos de cada
trabalhador estão livres, mesmo quando um trabalhador poderia ter iniciado antes
sozinho; validate_schedule rejeita uma gangue cujos trabalhadores não coiniciam
(schedule.feasibility.gang_cosched). Um trabalhador de gangue é rígido (não
moldável), já que uma gangue coinicia com uma largura fixa; uma tarefa independente
deixa gang_id indefinido.
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)
)A família do contínuo distributed-training-gang exercita essa alavanca: gangues de
dois a quatro trabalhadores reutilizam um pool de aceleradores compartilhado e os
trabalhos chegam ao longo do tempo, de modo que um trabalho não pode começar até que
haja aceleradores suficientes livres simultaneamente e a ordem do escalonamento
decide qual trabalho adquire primeiro seu conjunto completo de trabalhadores.
⚖️ Repartição equitativa multi-inquilino
As tarefas carregam um TaskSpec.tenant_id opcional rotulando o inquilino
proprietário para a contabilidade de repartição equitativa multi-inquilino. O
objetivo dominant-resource-share (ObjectiveKind.DOMINANT_RESOURCE_SHARE, segundo
a equidade de recurso dominante de Ghodsi et al.) pontua quão uniformemente as cotas
dominantes dos inquilinos são equilibradas: a cota dominante de cada inquilino é a
maior fração, entre recursos, da capacidade-tempo total de um recurso que suas
tarefas ocupam, e o objetivo é a dispersão entre o inquilino mais e o menos atendido
— 0.0 para um escalonamento de cota-dominante-igual (equitativo), maior quando um
inquilino monopoliza seu recurso dominante. O objetivo é de sentido minimizar e
retorna DEFERRED em uma instância sem inquilino. Um tenant_id é ortogonal à
execução em gangue, moldável e imprecisa — as tarefas de um inquilino podem ser
qualquer uma delas; uma tarefa sem inquilino deixa tenant_id indefinido.
A alavanca é o posicionamento, não a ordem do escalonamento: a ocupação total de
recursos de um inquilino é fixada por sua carga de trabalho, então sobre quais
recursos suas tarefas caem é o que equilibra ou enviesa as cotas dominantes. O
executável
examples/multi_tenant_fairshare_study.py
pontua um posicionamento equitativo (disperso) versus um monopolizador de um
inquilino pesado e um leve em um pool de três nós, tornando a alavanca explícita.
⏱️ Prazos rígidos e flexíveis
Uma tarefa pode carregar um prazo de conclusão TaskSpec.deadline, e
TaskSpec.deadline_kind classifica como uma perda é julgada. O padrão
ConstraintKind.HARD torna um prazo perdido uma violação de viabilidade:
validate_schedule levanta um problema bloqueante schedule.feasibility.deadline e
o escalonamento se reporta inviável. ConstraintKind.SOFT torna a mesma perda apenas
uma penalidade de atraso — o escalonamento permanece viável enquanto o excesso se
acumula ao objetivo lateness (ObjectiveKind.LATENESS), de modo que uma tarefa de
tempo-real-flexível é penalizada por atraso sem tornar o escalonamento inviável. O
tipo não tem efeito quando deadline está indefinido.
As duas leituras de um prazo são independentes: um prazo rígido limita a região
viável enquanto um prazo flexível molda a superfície do objetivo, e uma tarefa pode
usar qualquer um. O executável
examples/infeasibility_study.py
percorre o caso do prazo rígido de ponta a ponta — uma instância sobre-restrita
reportada inviável, e a ordem menos-inviável quando nenhum escalonamento viável
existe.
💰 Modelos de custo
O escalonamento consciente de custo é descrito por um CostModel, um artefato
imutável separado, vinculado a um problema por seu identificador. Manter a camada de
custo fora de ProblemSpec permite que um problema e seus dados de custo versionem e
serializem independentemente. Um modelo de custo agrupa cinco contratos opcionais:
ExecutionTimeMatrix— tempos de processamento de máquina-não-relacionada (p_ij) para a famíliaR||Cmax. A matriz é esparsa: um par(task, machine)ausente significa que a tarefa não pode rodar ali, e consultá-lo levanta um erro em vez de assumir zero por padrão.CompatibilityMask— um conjunto explícito de elegibilidade tarefa-para-máquina, mantido distinto da matriz de execução para que elegibilidade e tempo tabulado possam divergir.SetupMatrix— tempos de preparação dependentes da sequência, indexados por transição tarefa-para-tarefa ou tipo-para-tipo em uma máquina.LoadModel— uma curva de execução dependente da carga que mapeia um nível de carga a um multiplicador de duração por interpolação linear por partes ou por degraus entre pontos de quebra estritamente crescentes.CommunicationModel— penalidades de comunicação entre recursos sobre um grafo esparso; a comunicação na mesma máquina é gratuita e pares entre-máquinas não listados recorrem a uma penalidade padrão 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 e reduções
A família de objetivos é nomeada em OBJECTIVE_FAMILY, que registra o sentido e a
unidade canônicos de cada objetivo: makespan, energia, custo, carbono, latência,
atraso, equidade, cota de recurso dominante (a dispersão entre a cota de recurso
dominante do inquilino mais e o menos atendido — repartição equitativa
multi-inquilino), confiabilidade, segurança, robustez, preparação, recompensa
imprecisa (a computação opcional concluída além da parte obrigatória de tarefas
parcialmente executáveis), e compostos ponderados. Um resultado medido é carregado
por VectorObjectiveValue, que contém uma ou mais entradas ObjectiveValue
(ordenadas por nome) mais uma ObjectiveReduction explícita e um rótulo de
divulgação. As reduções de soma-ponderada e de Chebyshev escalarizam o vetor; as
reduções não escalarizantes (NONE, LEXICOGRAPHIC) levantam um erro em
scalarize() para que os chamadores tratem a ordenação explicitamente.
✅ Relatório de viabilidade
build_feasibility_report transforma um relatório de validação de escalonamento em um
FeasibilityReport visível ao revisor: violações rígidas se tornam registros
InfeasibleRow por linha, e restrições flexíveis violadas nomeadas acumulam entradas
SoftConstraintPenalty ponderadas. Uma violação flexível nunca inverte feasible
para False.
📐 Avaliação de objetivos, restrições e fronteiras
A camada de objetivos-e-restrições computa cada objetivo nomeado individualmente, de
modo que nenhum é dobrado em um pacote genérico. evaluate_objective deriva
makespan, atraso, equidade de carga (índice de Jain) e — para instâncias rotuladas
por inquilino — equidade de cota de recurso dominante diretamente de um
escalonamento, deriva o custo de uma matriz de tempo de execução anexada e o tempo de
preparação dependente da sequência de uma matriz de preparação anexada, e reduz os
componentes computados em um composto ponderado. Objetivos que precisam de um modelo
que o núcleo não carrega (energia, carbono, latência, confiabilidade, segurança,
robustez) retornam uma ObjectiveEvaluation com status DEFERRED e uma justificativa
nomeada em vez de um valor fabricado. MultiObjectiveOutcome carrega os valores
escalares, o objetivo vetorial e o relatório de viabilidade através de uma viagem de
ida e volta de serialização determinística.
A contabilidade de restrições adiciona a restrição de nível de serviço (SLA) nomeada
como uma restrição de primeira classe com tanto um caminho de quebra rígida (uma
inviabilidade) quanto um caminho de penalidade flexível (uma penalidade ponderada
proporcional à quebra). Um ConstraintViolationSummary agrega violações rígidas,
penalidades flexíveis, resultados de SLA e diagnósticos de reparo; ele reporta
inviável seja em uma violação de regra rígida, seja em uma quebra de SLA rígida.
Os auxiliares de Pareto extraem a frente não dominada e reportam os quatro
indicadores de qualidade nomeados selecionados segundo Riquelme, Von Lücken & Barán
(2015): hipervolume (o indicador primário), IGD+ (um indicador de convergência
fracamente Pareto-conforme), o epsilon-indicador aditivo, e a dispersão (o indicador
de diversidade). Cada indicador é computável individualmente. frontier_data
constrói registros prontos para a fronteira com um sinalizador não-dominado para
análise e renderização do portal.
A robustez é quantificada como um objetivo mensurável em vez de um rótulo de postura:
evaluate_robustness agrega o valor de um objetivo sobre um conjunto de perturbações
declarado explicitamente, seja como o valor de pior caso, seja como o valor em risco
condicional (CVaR) da pior cauda. Tanto o conjunto de perturbações quanto a agregação
são registrados no resultado.
⚠️ Limitações
O núcleo do kernel define contratos de custo, objetivo e restrição e a camada de
objetivos-e-restrições que os avalia; ele não otimiza. Objetivos que precisam de um
modelo que o núcleo não carrega (energia, carbono, latência, confiabilidade,
segurança e robustez) são reportados como adiados com uma justificativa nomeada em
vez de estimados. O LoadModel avalia apenas sua própria curva declarada, e
ObjectiveDefinition carrega metadados em vez de um avaliador.
🌱 Proveniência e sementes
Os artefatos gerados carregam registros Provenance, ArtifactHash,
SourceReference, EnvironmentStamp e SeedLineage opcional. Os fluxos de sementes
são derivados de coordenadas estáveis:
from dispatchatlas.core import derive_seed
seed = derive_seed(42, "benchmark.smoke", 0)A mesma semente raiz, espaço de nomes e índice sempre produzem a mesma semente derivada.
📦 Serialização
Os objetos do núcleo serializam através de mapeamentos canônicos compatíveis com JSON
e invólucros ArtifactEnvelope. Os invólucros vinculam as cargas úteis a hashes de
conteúdo e copiam esse hash de volta à proveniência.
🔌 Protocolos
Os pacotes externos dependem de protocolos definidos no núcleo:
BenchmarkProviderSolverExperimentRunnerResultRepositoryDisclosurePolicyAnalysisExporter
Esses protocolos mantêm as importações de pacotes direcionadas para dentro ao mesmo tempo que permitem que pacotes concretos de benchmark, solver, experimento e análise se componham mais tarde.