Перейти к содержимому
DispatchAtlas
Поиск

Контракты предметной области

dispatchatlas.core — это внутренняя граница пакета. Он использует неизменяемые объекты исключительно из стандартной библиотеки, чтобы бенчмарки, решатели, эксперименты, анализ и документация потребляли единый общий словарь планирования.

🗂️ Модель планирования

Задачи представляются с помощью ProblemSpec, TaskSpec, ResourceSpec, Dependency, Objective и Constraint. Кандидатные расписания используют Assignment, Schedule, ObjectiveValue и ScheduleResult.

Валидация явная:

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 возвращает ValidatedProblem со штампом валидации и машиночитаемым отчётом. validate_schedule проверяет покрытие задач, ёмкость ресурсов, временные границы и зависимости.

🔗 Многоресурсная со-аллокация

Задача может требовать более одного ресурса одновременно. TaskSpec.demands — это кортеж ResourceRequirement, и конструктор расписания со-аллоцирует задачу на каждый ресурс, который она требует, удерживая их вместе на всю длительность задачи. Поэтому кандидатное расписание записывает каждое размещение как Assignment.resource_ids — кортеж, а не единственный ресурс — и validate_schedule подтверждает, что две перекрывающиеся во времени задачи никогда не делят ресурс.

Граф конфликта ресурсов — это рычаг планирования: две задачи, чьи множества требований пересекаются, должны сериализоваться, а две с непересекающимися множествами выполняются параллельно.

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

Семейства континуума accelerator-coscheduling, distributed-transaction и fpga-partitioning задействуют этот рычаг — первое с фиксированным удержанием вычислений плюс ускоритель, второе с набором блокировок переменной мощности над фрагментами данных. Исполняемый examples/inspect_coallocation.py планирует оба и показывает задачи с непересекающимися ресурсами, выполняющиеся параллельно, тогда как задачи, делящие ресурсы, сериализуются.

🧩 Формуемое выполнение

Задача может выполняться в одном из нескольких режимов. TaskSpec.modes — это кортеж TaskMode, каждый из которых пара (demands, duration), где более широкий режим (требующий больше ресурсов) выполняется быстрее — формуемое ускорение. Когда modes задано, задача формуема: конструктор расписания выбирает для каждой задачи режим, завершающийся раньше всего при имеющихся свободных ресурсах, так что порядок расписания решает, сколько параллелизма требует каждая задача. Жёсткая задача оставляет modes незаданным (по умолчанию), и её duration и demands верхнего уровня являются её единственным режимом, который точный бэкенд планирует как консервативную эталонную точку. validate_schedule подтверждает, что каждое размещение соответствует одному объявленному режиму, а формуемое и неточное (mandatory_duration) выполнение взаимоисключающи — задача выбирает свой параллелизм или отбрасывает необязательную работу, но не оба.

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

Семейство континуума elastic-serverless-autoscale задействует этот рычаг: каждый вызов функции может масштабироваться до одного, двух или четырёх работников, взятых из небольшого общего пула всплеска, так что порядок расписания решает, какие вызовы получают дефицитные широкие-и-быстрые режимы, а какие выполняются узко — компромисс между задержкой и стоимостью ресурсов.

🛰️ Со-планирование банды

Задачи, разделяющие TaskSpec.gang_id, образуют банду, чьи работники должны все стартовать одновременно на различных ресурсах — со-старт по принципу всё-или-ничего, как синхронному распределённому-обучению или MPI-заданию нужны его работники, выполняющиеся вместе. Последовательный конструктор размещает целую банду атомарно в самый ранний момент, когда ресурсы каждого работника свободны, даже когда один работник мог бы стартовать раньше в одиночку; validate_schedule отвергает банду, чьи работники не со-стартуют (schedule.feasibility.gang_cosched). Работник банды жёсткий (не формуемый), поскольку банда со-стартует с фиксированной шириной; независимая задача оставляет gang_id незаданным.

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

Семейство континуума distributed-training-gang задействует этот рычаг: банды от двух до четырёх работников переиспользуют общий пул ускорителей, а задания прибывают со временем, так что задание не может начаться, пока не освободится одновременно достаточно ускорителей, и порядок расписания решает, какое задание первым получит свой полный набор работников.

⚖️ Многоарендное справедливое распределение

Задачи несут необязательный TaskSpec.tenant_id, помечающий владеющего арендатора для учёта многоарендного справедливого распределения. Цель dominant-resource-share (ObjectiveKind.DOMINANT_RESOURCE_SHARE, по справедливости доминирующего ресурса Ghodsi et al.) оценивает, насколько равномерно сбалансированы доминирующие доли арендаторов: доминирующая доля каждого арендатора — это наибольшая доля, среди ресурсов, общей ёмкости-времени ресурса, которую занимают его задачи, а цель — разброс между наиболее и наименее обслуженным арендатором — 0.0 для расписания с равными доминирующими долями (справедливого), выше, когда один арендатор монополизирует свой доминирующий ресурс. Цель в смысле минимизации и возвращает DEFERRED на инстансе без арендаторов. tenant_id ортогонален бандному, формуемому и неточному выполнению — задачи арендатора могут быть любыми из них; задача без арендатора оставляет tenant_id незаданным.

Рычаг — это размещение, а не порядок расписания: общая занятость ресурсов арендатором зафиксирована его рабочей нагрузкой, поэтому то, на какие ресурсы попадают его задачи, и есть то, что балансирует или перекашивает доминирующие доли. Исполняемый examples/multi_tenant_fairshare_study.py оценивает справедливое (разнесённое) против монополизирующего размещение тяжёлого и лёгкого арендатора на пуле из трёх узлов, делая рычаг явным.

⏱️ Жёсткие и мягкие дедлайны

Задача может нести дедлайн завершения TaskSpec.deadline, а TaskSpec.deadline_kind классифицирует, как судится пропуск. По умолчанию ConstraintKind.HARD делает пропущенный дедлайн нарушением допустимости: validate_schedule поднимает блокирующую проблему schedule.feasibility.deadline, и расписание сообщается недопустимым. ConstraintKind.SOFT делает тот же пропуск только штрафом за опоздание — расписание остаётся допустимым, пока превышение накапливается к цели lateness (ObjectiveKind.LATENESS), так что задача мягкого реального времени штрафуется за опоздание, не делая расписание недопустимым. Этот вид не имеет эффекта, когда deadline не задан.

Два прочтения дедлайна независимы: жёсткий дедлайн ограничивает допустимую область, тогда как мягкий дедлайн формирует поверхность цели, и задача может использовать любой. Исполняемый examples/infeasibility_study.py проходит случай жёсткого дедлайна от начала до конца — переограниченный инстанс, сообщённый недопустимым, и наименее-недопустимый порядок, когда допустимого расписания не существует.

💰 Модели стоимости

Стоимостно-осознанное планирование описывается CostModel — отдельным неизменяемым артефактом, связанным с задачей по её идентификатору. Удержание слоя стоимости вне ProblemSpec позволяет задаче и её данным о стоимости версионироваться и сериализоваться независимо. Модель стоимости связывает пять необязательных контрактов:

  • ExecutionTimeMatrix — времена обработки несвязанных машин (p_ij) для семейства R||Cmax. Матрица разрежена: отсутствующая пара (task, machine) означает, что задача не может там выполняться, и её запрос поднимает ошибку, а не возвращает ноль по умолчанию.
  • CompatibilityMask — явное множество пригодности задача-к-машине, удерживаемое отдельно от матрицы выполнения, чтобы пригодность и табулированное время могли расходиться.
  • SetupMatrix — зависящие от последовательности времена настройки, индексированные по переходу задача-к-задаче или тип-к-типу на машине.
  • LoadModel — зависящая от нагрузки кривая выполнения, отображающая уровень нагрузки в множитель длительности через кусочно-линейную или ступенчатую интерполяцию между строго возрастающими точками перелома.
  • CommunicationModel — штрафы межресурсной коммуникации на разреженном графе; коммуникация на той же машине бесплатна, а неперечисленные межмашинные пары откатываются к объявленному штрафу по умолчанию.
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)]
    ),
)

🎯 Цели и редукции

Семейство целей именуется в OBJECTIVE_FAMILY, который записывает канонический смысл и единицу каждой цели: makespan, энергия, стоимость, углерод, задержка, опоздание, справедливость, доля доминирующего ресурса (разброс между долей доминирующего ресурса наиболее и наименее обслуженного арендатора — многоарендное справедливое распределение), надёжность, безопасность, робастность, настройка, неточная награда (необязательное вычисление, завершённое сверх обязательной части частично исполняемых задач) и взвешенные композиты. Измеренный результат несётся VectorObjectiveValue, который содержит одну или более записей ObjectiveValue (упорядоченных по имени) плюс явную ObjectiveReduction и метку раскрытия. Редукции взвешенной суммы и Чебышёва скаляризуют вектор; нескаляризующие редукции (NONE, LEXICOGRAPHIC) поднимают ошибку на scalarize(), чтобы вызывающие обрабатывали упорядочивание явно.

Отчёт о допустимости

build_feasibility_report превращает отчёт валидации расписания в видимый рецензенту FeasibilityReport: жёсткие нарушения становятся построчными записями InfeasibleRow, а именованные нарушенные мягкие ограничения накапливают взвешенные записи SoftConstraintPenalty. Мягкое нарушение никогда не переключает feasible в False.

📐 Оценка целей, ограничения и фронты

Слой целей-и-ограничений вычисляет каждую именованную цель индивидуально, так что ни одна не свёрнута в обобщённый пакет. evaluate_objective выводит makespan, опоздание, справедливость нагрузки (индекс Джейна) и — для инстансов, помеченных арендатором — справедливость доли доминирующего ресурса прямо из расписания, выводит стоимость из присоединённой матрицы времени выполнения и зависящее от последовательности время настройки из присоединённой матрицы настройки, и редуцирует вычисленные компоненты во взвешенный композит. Цели, которым нужна модель, не несомая ядром (энергия, углерод, задержка, надёжность, безопасность, робастность), возвращают ObjectiveEvaluation со статусом DEFERRED и именованным обоснованием вместо сфабрикованного значения. MultiObjectiveOutcome несёт скалярные значения, векторную цель и отчёт о допустимости через детерминированный цикл сериализации.

Учёт ограничений добавляет именованное ограничение уровня обслуживания (SLA) как первоклассное ограничение с путём жёсткого нарушения (недопустимость) и путём мягкого штрафа (взвешенный штраф, пропорциональный нарушению). ConstraintViolationSummary агрегирует жёсткие нарушения, мягкие штрафы, итоги SLA и диагностику ремонта; он сообщает недопустимость либо при жёстком нарушении правила, либо при жёстком нарушении SLA.

Помощники Парето извлекают недоминируемый фронт и сообщают четыре именованных индикатора качества, выбранных по Riquelme, Von Lücken & Barán (2015): гиперобъём (первичный индикатор), IGD+ (слабо-Парето-совместимый индикатор сходимости), аддитивный эпсилон-индикатор и разброс (индикатор разнообразия). Каждый индикатор вычислим индивидуально. frontier_data строит готовые для фронта записи с флагом недоминируемости для анализа и рендеринга портала.

Робастность количественно выражается как измеримая цель, а не как метка-поза: evaluate_robustness агрегирует значение одной цели по явно объявленному множеству возмущений — либо как значение наихудшего случая, либо как условную стоимость под риском (CVaR) наихудшего хвоста. И множество возмущений, и агрегация записываются в результат.

⚠️ Ограничения

Ядро определяет контракты стоимости, цели и ограничения и слой целей-и-ограничений, который их оценивает; оно не оптимизирует. Цели, которым нужна модель, не несомая ядром (энергия, углерод, задержка, надёжность, безопасность и робастность), сообщаются как отложенные с именованным обоснованием, а не оцениваются. LoadModel оценивает только свою собственную объявленную кривую, а ObjectiveDefinition несёт метаданные, а не оценщик.

🌱 Происхождение и зёрна

Сгенерированные артефакты несут записи Provenance, ArtifactHash, SourceReference, EnvironmentStamp и необязательную SeedLineage. Потоки зёрен выводятся из стабильных координат:

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

Одни и те же корневое зерно, пространство имён и индекс всегда производят одно и то же производное зерно.

📦 Сериализация

Объекты ядра сериализуются через канонические JSON-совместимые отображения и обёртки ArtifactEnvelope. Обёртки связывают полезные нагрузки с хешами содержимого и копируют этот хеш обратно в происхождение.

🔌 Протоколы

Внешние пакеты зависят от протоколов, определённых в ядре:

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

Эти протоколы держат импорты пакетов направленными внутрь, позволяя при этом конкретным пакетам бенчмарков, решателей, экспериментов и анализа компоноваться позже.