Pular para o conteúdo
DispatchAtlas
Buscar

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ília R||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:

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

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.