Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 13 additions & 16 deletions scripts/benchmark_pressure.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
"""Benchmark exact pressure lower bounds on vector-clock workloads.

Compares the shipping ``get_pressure`` (linearize then sweep, else the exact
antichain) against the two exact C++ methods:
antichain, under the default work budget) against the two exact C++ methods:

- ``get_antichain_pressure``: max-weight antichain (weighted Dilworth via
min flow), the tightest sound lower bound on any placement's peak and the
reference every ratio is measured against
- ``get_pressure(work_budget=None)``: unbudgeted max-weight antichain
(weighted Dilworth via min flow), the tightest sound lower bound on any
placement's peak and the reference every ratio is measured against
- ``get_closure_pressure``: realizable peak via join-closure enumeration;
instances whose closure exceeds ``--closure-cap`` are reported as capped
and excluded from the means. Instances where ``get_pressure`` exceeds its
Expand Down Expand Up @@ -50,13 +50,13 @@
from omnimalloc.benchmark.sources.sync_patterns import SYNC_PATTERNS, SyncPatternSource
from omnimalloc.benchmark.timer import Timer
from omnimalloc.primitives import (
get_antichain_pressure,
get_closure_pressure,
get_per_allocation_closure_pressure,
get_per_allocation_placement_pressure,
get_per_allocation_pressure,
get_placement_pressure,
get_pressure,
)
from omnimalloc.primitives.pressure import get_pressure

if TYPE_CHECKING:
from collections.abc import Callable
Expand All @@ -70,7 +70,7 @@

SIZES = (100, 300, 1_000, 3_000, 10_000)
NUM_SYNCS = (0, 64, 1_024)
REFERENCE = "get_antichain_pressure"
REFERENCE = "get_pressure(work_budget=None)"
PER_ALLOCATION_REFERENCE = "get_per_allocation_pressure"
METHODS = (
"get_pressure",
Expand All @@ -92,7 +92,7 @@
AXIS = "#c3c2b7"
COLORS = {
"get_pressure": "#2a78d6",
"get_antichain_pressure": "#1baf7a",
REFERENCE: "#1baf7a",
"get_closure_pressure": "#eda100",
"omni_allocator": "#4a3aa7",
"get_per_allocation_pressure": "#1baf7a",
Expand All @@ -111,11 +111,6 @@
Sample = dict[str, Any]


def _peak(placed: tuple[Allocation, ...]) -> int:
heights = [alloc.height for alloc in placed if alloc.height is not None]
return max(heights, default=0)


def _capped(
query: Callable[..., Any], allocations: tuple[Allocation, ...], cap: int
) -> Value:
Expand All @@ -142,11 +137,13 @@ def _sample_runners(
) -> dict[str, Runner]:
return {
"get_pressure": lambda: _budgeted(allocations),
REFERENCE: lambda: get_antichain_pressure(allocations),
REFERENCE: lambda: get_pressure(allocations, work_budget=None),
"get_closure_pressure": lambda: _capped(
get_closure_pressure, allocations, args.closure_cap
),
"omni_allocator": lambda: _peak(allocator.allocate(allocations)),
"omni_allocator": lambda: get_placement_pressure(
allocator.allocate(allocations)
),
PER_ALLOCATION_REFERENCE: lambda: get_per_allocation_pressure(allocations),
"get_per_allocation_closure_pressure": lambda: _capped(
get_per_allocation_closure_pressure, allocations, args.closure_cap
Expand Down Expand Up @@ -235,7 +232,7 @@ def _check_per_allocation(
if closure and values.get("get_closure_pressure") is not None:
assert max(closure.values()) == values["get_closure_pressure"]
if placement:
assert max(placement.values()) == _peak(placed)
assert max(placement.values()) == get_placement_pressure(placed)


def _ratios(values: dict[str, Any], reference: int) -> dict[str, float]:
Expand Down
14 changes: 8 additions & 6 deletions src/cpp/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -123,19 +123,21 @@ NB_MODULE(_cpp, m) {
m.def("compute_conflict_degrees", &compute_conflict_degrees, "allocations"_a,
nb::call_guard<nb::gil_scoped_release>(), nb::rv_policy::move);
m.def("try_linearize", &try_linearize, "allocations"_a,
"work_budget"_a = kNoLinearizeBudget,
"work_budget"_a = kNoWorkBudget,
nb::call_guard<nb::gil_scoped_release>(), nb::rv_policy::move);
m.attr("DEFAULT_WORK_BUDGET") = kDefaultWorkBudget;
m.attr("DEFAULT_CLOSURE_CAP") = kDefaultClosureCap;
m.def("antichain_pressure", &antichain_pressure, "allocations"_a,
"work_budget"_a = kNoLinearizeBudget,
"work_budget"_a = kNoWorkBudget,
nb::call_guard<nb::gil_scoped_release>());
m.def("closure_pressure", &closure_pressure, "allocations"_a, "closure_cap"_a,
m.def("closure_pressure", &closure_pressure, "allocations"_a,
"closure_cap"_a = kDefaultClosureCap,
nb::call_guard<nb::gil_scoped_release>());
m.def("per_allocation_antichain_pressure", &per_allocation_antichain_pressure,
"allocations"_a, nb::call_guard<nb::gil_scoped_release>(),
nb::rv_policy::move);
"allocations"_a, "work_budget"_a = kNoWorkBudget,
nb::call_guard<nb::gil_scoped_release>(), nb::rv_policy::move);
m.def("per_allocation_closure_pressure", &per_allocation_closure_pressure,
"allocations"_a, "closure_cap"_a,
"allocations"_a, "closure_cap"_a = kDefaultClosureCap,
nb::call_guard<nb::gil_scoped_release>(), nb::rv_policy::move);
m.def("per_allocation_placement_pressure", &per_allocation_placement_pressure,
"allocations"_a, "clique_cap"_a = false,
Expand Down
6 changes: 3 additions & 3 deletions src/cpp/primitives/antichain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ int64_t antichain_pressure(const std::vector<Allocation>& allocations,
}

std::vector<int64_t> per_allocation_antichain_pressure(
const std::vector<Allocation>& allocations) {
const std::vector<Allocation>& allocations, uint64_t work_budget) {
const size_t n = allocations.size();
if (n == 0) {
return {};
Expand All @@ -269,7 +269,7 @@ std::vector<int64_t> per_allocation_antichain_pressure(
// Interval orders: every clique through an allocation shares a time point
// inside its own lifetime (Helly), so the pinned antichain is the window
// peak of the linearized sweep.
if (const auto times = linearize_times(allocations)) {
if (const auto times = linearize_times(allocations, work_budget)) {
std::vector<int64_t> weights(n);
std::ranges::transform(allocations, weights.begin(), &Allocation::size);
return interval_peaks(*times, weights);
Expand Down Expand Up @@ -302,7 +302,7 @@ std::vector<int64_t> per_allocation_antichain_pressure(
weights.push_back(groups.weights[j]);
}
pinned[i] = groups.weights[i] +
max_antichain(starts, ends, weights, d, 1, kNoLinearizeBudget);
max_antichain(starts, ends, weights, d, 1, work_budget);
});

std::vector<int64_t> peaks(n);
Expand Down
7 changes: 5 additions & 2 deletions src/cpp/primitives/antichain.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ namespace omnimalloc {
// of stalling or exhausting memory once the budget is exceeded.
[[nodiscard]] int64_t antichain_pressure(
const std::vector<Allocation>& allocations,
uint64_t work_budget = kNoLinearizeBudget);
uint64_t work_budget = kNoWorkBudget);

// Exact per-allocation pressure, aligned with `allocations`: for each
// allocation the max-weight antichain through it, i.e. the heaviest
Expand All @@ -34,7 +34,10 @@ namespace omnimalloc {
// Interval orders resolve through one linearized window sweep; genuinely
// partial orders solve one pinned min flow per distinct lifetime over its
// conflict neighborhood — built for certification, not the 10k+ hot path.
// A finite `work_budget` bounds the linearize attempt and each pinned flow
// (see antichain_pressure); the flow path throws once it is exceeded.
[[nodiscard]] std::vector<int64_t> per_allocation_antichain_pressure(
const std::vector<Allocation>& allocations);
const std::vector<Allocation>& allocations,
uint64_t work_budget = kNoWorkBudget);

} // namespace omnimalloc
13 changes: 9 additions & 4 deletions src/cpp/primitives/closure.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,21 @@

namespace omnimalloc {

// Default join-closure enumeration cap, so huge vector-clock instances
// fail fast instead of exhausting memory. Exported to Python as
// DEFAULT_CLOSURE_CAP.
inline constexpr size_t kDefaultClosureCap = 1 << 14;

// Exact realizable peak: the maximum total size jointly live at a single
// cut, scored over the join-closure of the birth clocks (the minimal cut
// where a candidate set is jointly live is the join of its births, and
// joins of observed clocks span the consistent-cut lattice). Note that
// pairwise-concurrent allocations need not share a cut, so this can sit
// strictly below antichain_pressure; both soundly lower-bound any
// placement's peak. nullopt once the closure exceeds `closure_cap`; the
// default cap policy lives on the Python side (`DEFAULT_CLOSURE_CAP`).
// placement's peak. nullopt once the closure exceeds `closure_cap`.
[[nodiscard]] std::optional<int64_t> closure_pressure(
const std::vector<Allocation>& allocations, size_t closure_cap);
const std::vector<Allocation>& allocations,
size_t closure_cap = kDefaultClosureCap);

// Exact realizable peak while each allocation is live, aligned with
// `allocations`: the maximum total size at any join-closure cut where the
Expand All @@ -32,6 +37,6 @@ namespace omnimalloc {
// equals closure_pressure. nullopt once the closure exceeds `closure_cap`.
[[nodiscard]] std::optional<std::vector<int64_t>>
per_allocation_closure_pressure(const std::vector<Allocation>& allocations,
size_t closure_cap);
size_t closure_cap = kDefaultClosureCap);

} // namespace omnimalloc
7 changes: 3 additions & 4 deletions src/cpp/primitives/linearize.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@

namespace omnimalloc {

inline constexpr uint64_t kNoLinearizeBudget =
std::numeric_limits<uint64_t>::max();
inline constexpr uint64_t kNoWorkBudget = std::numeric_limits<uint64_t>::max();

// Default dominance-counting budget for implicit and hot-path callers (the
// omni allocator's linearize attempt, `Pool.pressure`), so huge vector-clock
Expand All @@ -30,13 +29,13 @@ inline constexpr uint64_t kDefaultWorkBudget = 100'000'000;
// (undecided; the caller falls back to the vector conflict engine).
[[nodiscard]] std::optional<std::vector<std::pair<int64_t, int64_t>>>
linearize_times(const std::vector<Allocation>& allocations,
uint64_t work_budget = kNoLinearizeBudget);
uint64_t work_budget = kNoWorkBudget);

// Allocation-level wrapper: allocations rebuilt with the surrogate scalar
// times, or nullopt when `linearize_times` yields none (not an interval
// order — or undecided under a finite `work_budget`).
[[nodiscard]] std::optional<std::vector<Allocation>> try_linearize(
const std::vector<Allocation>& allocations,
uint64_t work_budget = kNoLinearizeBudget);
uint64_t work_budget = kNoWorkBudget);

} // namespace omnimalloc
3 changes: 2 additions & 1 deletion src/python/omnimalloc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,16 @@
from .primitives import System as System
from .primitives import TimePoint as TimePoint
from .primitives import VectorClock as VectorClock
from .primitives import get_antichain_pressure as get_antichain_pressure
from .primitives import get_closure_pressure as get_closure_pressure
from .primitives import get_conflicts as get_conflicts
from .primitives import (
get_per_allocation_closure_pressure as get_per_allocation_closure_pressure,
)
from .primitives import (
get_per_allocation_placement_pressure as get_per_allocation_placement_pressure,
)
from .primitives import get_per_allocation_pressure as get_per_allocation_pressure
from .primitives import get_placement_pressure as get_placement_pressure
from .primitives import get_pressure as get_pressure
from .primitives import try_linearize as try_linearize
from .validate import validate_allocation as validate_allocation
Expand Down
8 changes: 5 additions & 3 deletions src/python/omnimalloc/_cpp.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,15 @@ def try_linearize(allocations: Sequence[Allocation], work_budget: int = 18446744

DEFAULT_WORK_BUDGET: int = 100000000

DEFAULT_CLOSURE_CAP: int = 16384

def antichain_pressure(allocations: Sequence[Allocation], work_budget: int = 18446744073709551615) -> int: ...

def closure_pressure(allocations: Sequence[Allocation], closure_cap: int) -> int | None: ...
def closure_pressure(allocations: Sequence[Allocation], closure_cap: int = 16384) -> int | None: ...

def per_allocation_antichain_pressure(allocations: Sequence[Allocation]) -> list[int]: ...
def per_allocation_antichain_pressure(allocations: Sequence[Allocation], work_budget: int = 18446744073709551615) -> list[int]: ...

def per_allocation_closure_pressure(allocations: Sequence[Allocation], closure_cap: int) -> list[int] | None: ...
def per_allocation_closure_pressure(allocations: Sequence[Allocation], closure_cap: int = 16384) -> list[int] | None: ...

def per_allocation_placement_pressure(allocations: Sequence[Allocation], clique_cap: bool = False) -> list[int]: ...

Expand Down
3 changes: 2 additions & 1 deletion src/python/omnimalloc/primitives/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
from .allocation import IdType as IdType
from .allocation import TimePoint as TimePoint
from .allocation import VectorClock as VectorClock
from .conflicts import get_conflicts as get_conflicts
from .linearize import try_linearize as try_linearize
from .memory import Memory as Memory
from .pool import Pool as Pool
from .pressure import get_antichain_pressure as get_antichain_pressure
from .pressure import get_closure_pressure as get_closure_pressure
from .pressure import (
get_per_allocation_closure_pressure as get_per_allocation_closure_pressure,
Expand All @@ -19,5 +19,6 @@
get_per_allocation_placement_pressure as get_per_allocation_placement_pressure,
)
from .pressure import get_per_allocation_pressure as get_per_allocation_pressure
from .pressure import get_placement_pressure as get_placement_pressure
from .pressure import get_pressure as get_pressure
from .system import System as System
22 changes: 22 additions & 0 deletions src/python/omnimalloc/primitives/conflicts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#
# SPDX-License-Identifier: Apache-2.0
#

from omnimalloc._cpp import compute_temporal_overlaps

from .allocation import Allocation, IdType
from .utils import ensure_unique_ids


def get_conflicts(allocations: tuple[Allocation, ...]) -> dict[IdType, set[IdType]]:
"""Temporal conflict map: each allocation's id to the ids it overlaps in time.

The relation every placement packs against: conflicting allocations must
occupy disjoint address ranges. Symmetric and total — every allocation is
a key, conflict-free ones map to an empty set. Handles scalar and
vector-clock lifetimes (mutually concurrent clocks conflict). C++ sweep
(allocators/first_fit.cpp).
"""
ensure_unique_ids(allocations)
overlaps = compute_temporal_overlaps(list(allocations))
return {alloc.id: overlaps.get(alloc.id, set()) for alloc in allocations}
Loading
Loading