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
9 changes: 5 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,12 @@ nanobind_add_module(
src/cpp/allocators/supermalloc/partition.cpp
src/cpp/allocators/tabu_search.cpp
src/cpp/allocators/telamalloc.cpp
src/cpp/analysis/antichain.cpp
src/cpp/analysis/closure.cpp
src/cpp/analysis/conflicts.cpp
src/cpp/analysis/linearize.cpp
src/cpp/analysis/placement.cpp
src/cpp/primitives/allocation.cpp
src/cpp/primitives/antichain.cpp
src/cpp/primitives/closure.cpp
src/cpp/primitives/linearize.cpp
src/cpp/primitives/placement.cpp
src/cpp/bindings.cpp)

target_include_directories(_cpp PRIVATE src/cpp)
Expand Down
8 changes: 4 additions & 4 deletions scripts/benchmark_pressure.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,17 @@

import matplotlib.pyplot as plt
from omnimalloc import OmniAllocator
from omnimalloc.benchmark.sources.concurrent_tiling import ConcurrentTilingSource
from omnimalloc.benchmark.sources.sync_patterns import SYNC_PATTERNS, SyncPatternSource
from omnimalloc.benchmark.timer import Timer
from omnimalloc.primitives import (
from omnimalloc.analysis import (
get_closure_pressure,
get_per_allocation_closure_pressure,
get_per_allocation_placement_pressure,
get_per_allocation_pressure,
get_placement_pressure,
get_pressure,
)
from omnimalloc.benchmark.sources.concurrent_tiling import ConcurrentTilingSource
from omnimalloc.benchmark.sources.sync_patterns import SYNC_PATTERNS, SyncPatternSource
from omnimalloc.benchmark.timer import Timer

if TYPE_CHECKING:
from collections.abc import Callable
Expand Down
4 changes: 2 additions & 2 deletions scripts/generate_readme_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@
from matplotlib.patches import Rectangle
from matplotlib.ticker import FuncFormatter, MultipleLocator
from omnimalloc import run_allocation, validate_allocation
from omnimalloc.allocators import DEFAULT_TIMEOUT, BaseAllocator
from omnimalloc.allocators import BaseAllocator
from omnimalloc.benchmark.sources import BaseSource
from omnimalloc.benchmark.timer import Timer
from omnimalloc.common.units import MB
from omnimalloc.common.constants import DEFAULT_TIMEOUT, MB

if TYPE_CHECKING:
from matplotlib.axes import Axes
Expand Down
2 changes: 1 addition & 1 deletion scripts/stress_omni.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@

import matplotlib.pyplot as plt
from omnimalloc.allocators import OmniAllocator
from omnimalloc.analysis.pressure import get_pressure
from omnimalloc.benchmark.sources.sync_patterns import SYNC_PATTERNS, SyncPatternSource
from omnimalloc.benchmark.timer import Timer
from omnimalloc.primitives.pressure import get_pressure

if TYPE_CHECKING:
from matplotlib.axes import Axes
Expand Down
107 changes: 0 additions & 107 deletions src/cpp/allocators/first_fit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@
#include "first_fit.hpp"

#include <algorithm>
#include <atomic>
#include <cstring>
#include <future>
#include <limits>
#include <numeric>
#include <span>
#include <stdexcept>
#include <string>
#include <tuple>
#include <utility>

#include "common/parallel.hpp"
Expand All @@ -31,111 +29,6 @@ void require_scalar_time(const std::vector<Allocation>& allocations,

namespace {

// Sweep line over the single timeline; valid only for all-scalar input.
OverlapIndices scalar_overlap_indices(
const std::vector<Allocation>& allocations) {
std::vector<std::tuple<int64_t, bool, size_t>> events;
events.reserve(allocations.size() * 2);
for (size_t i = 0; i < allocations.size(); ++i) {
events.emplace_back(allocations[i].start(), true, i);
events.emplace_back(allocations[i].end(), false, i);
}

// Sort events by time; ends sort before starts at equal times, matching the
// half-open interval semantics of Allocation::overlaps_temporally
std::sort(events.begin(), events.end());

OverlapIndices indices(allocations.size());
std::vector<size_t> active;
for (const auto& [time, is_start, idx] : events) {
if (is_start) {
// Current allocation overlaps with all currently active allocations
for (size_t active_idx : active) {
indices[idx].push_back(active_idx);
indices[active_idx].push_back(idx);
}
active.push_back(idx);
} else {
active.erase(std::find(active.begin(), active.end(), idx));
}
}

return indices;
}

// The pruned pairwise sweep itself lives in primitives/clock_rows.hpp
// (ConflictSweep), shared with the exact per-allocation pressure kernels.
ConflictSweep build_conflict_sweep(const std::vector<Allocation>& allocations) {
const ClockSpans spans = gather_clock_spans(allocations);
return {spans.starts, spans.ends, spans.dim};
}

} // namespace

CsrAdjacency build_conflict_adjacency(
const std::vector<Allocation>& allocations) {
const ConflictSweep sweep = build_conflict_sweep(allocations);
return sweep.adjacency(parallel_threads(sweep.count()));
}

namespace {

// Pairwise happens-before adjacency; the only option for vector clocks
OverlapIndices vector_overlap_indices(
const std::vector<Allocation>& allocations) {
const CsrAdjacency adj = build_conflict_adjacency(allocations);
OverlapIndices indices(allocations.size());
for (size_t i = 0; i < allocations.size(); ++i) {
indices[i].assign(adj.neighbors.begin() + adj.offsets[i],
adj.neighbors.begin() + adj.offsets[i + 1]);
}
return indices;
}

TemporalOverlaps overlaps_from_indices(
const std::vector<Allocation>& allocations, const OverlapIndices& indices) {
TemporalOverlaps overlaps;
for (size_t i = 0; i < allocations.size(); ++i) {
for (size_t j : indices[i]) {
overlaps[allocations[i].id()].insert(allocations[j].id());
}
}
return overlaps;
}

} // namespace

OverlapIndices compute_overlap_indices(
const std::vector<Allocation>& allocations) {
const bool all_scalar =
std::ranges::all_of(allocations, &Allocation::is_scalar_time);
return all_scalar ? scalar_overlap_indices(allocations)
: vector_overlap_indices(allocations);
}

TemporalOverlaps compute_temporal_overlaps(
const std::vector<Allocation>& allocations) {
return overlaps_from_indices(allocations,
compute_overlap_indices(allocations));
}

std::vector<int64_t> compute_conflict_degrees(
const std::vector<Allocation>& allocations) {
const ConflictSweep sweep = build_conflict_sweep(allocations);
std::vector<std::atomic<int64_t>> degree(sweep.count());
sweep.for_each_pair(parallel_threads(sweep.count()), [&](size_t i, size_t j) {
degree[i].fetch_add(1, std::memory_order_relaxed);
degree[j].fetch_add(1, std::memory_order_relaxed);
});
std::vector<int64_t> degrees(sweep.count());
for (size_t i = 0; i < sweep.count(); ++i) {
degrees[i] = degree[i].load(std::memory_order_relaxed);
}
return degrees;
}

namespace {

// Occupied (offset, end) span of a placed allocation, matching the span
// shape that `first_fit_offset` consumes
using Interval = std::pair<int64_t, int64_t>;
Expand Down
32 changes: 1 addition & 31 deletions src/cpp/allocators/first_fit.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,19 @@

#include <cstdint>
#include <optional>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>

#include "analysis/conflicts.hpp"
#include "primitives/allocation.hpp"
#include "primitives/clock_rows.hpp"
#include "primitives/id_type.hpp"

namespace omnimalloc {

// Temporal overlap adjacency: allocation id -> ids of overlapping allocations
using TemporalOverlaps =
std::unordered_map<IdType, std::unordered_set<IdType, IdTypeHash>,
IdTypeHash>;

// Index-based temporal adjacency: position i -> positions overlapping i
using OverlapIndices = std::vector<std::vector<size_t>>;

// Throw unless every allocation has scalar (interval) lifetimes; `who` names
// the rejecting entry point in the message.
void require_scalar_time(const std::vector<Allocation>& allocations,
const char* who);

// Map each allocation id to the ids of temporally overlapping allocations
[[nodiscard]] TemporalOverlaps compute_temporal_overlaps(
const std::vector<Allocation>& allocations);

// Map each allocation index to the indices of temporally overlapping
// allocations
[[nodiscard]] OverlapIndices compute_overlap_indices(
const std::vector<Allocation>& allocations);

// Occupied (offset, end) spans of the already-placed neighbors of one
// allocation, sorted by offset so the gap scans can go left-to-right
void gather_spans(const std::vector<size_t>& neighbors,
Expand All @@ -50,16 +30,6 @@ void gather_spans(const std::vector<size_t>& neighbors,
[[nodiscard]] int64_t first_fit_offset(
int64_t size, const std::vector<std::pair<int64_t, int64_t>>& spans);

// Per-allocation count of temporally overlapping allocations, aligned with
// `allocations`. Counts with multiplicity, so duplicate ids stay distinct.
[[nodiscard]] std::vector<int64_t> compute_conflict_degrees(
const std::vector<Allocation>& allocations);

// Pairwise happens-before conflict adjacency over the pruned vector sweep;
// handles scalar and vector-clock lifetimes alike.
[[nodiscard]] CsrAdjacency build_conflict_adjacency(
const std::vector<Allocation>& allocations);

// Offsets (aligned with `allocations`) and peak of the winning placement.
struct PortfolioPlacement {
std::vector<int64_t> offsets;
Expand Down
9 changes: 4 additions & 5 deletions src/cpp/allocators/omni.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
#include <algorithm>
#include <optional>

#include "analysis/linearize.hpp"
#include "first_fit.hpp"
#include "primitives/linearize.hpp"

namespace omnimalloc {

Expand All @@ -28,7 +28,7 @@ std::vector<Allocation> OmniAllocator::allocate(
std::ranges::all_of(allocations, &Allocation::is_scalar_time);
std::optional<std::vector<Allocation>> surrogates;
if (!all_scalar) {
surrogates = try_linearize(allocations, kDefaultWorkBudget);
surrogates = try_linearize(allocations, linearize_budget_);
}
const std::vector<Allocation>& problem =
surrogates.has_value() ? *surrogates : allocations;
Expand All @@ -49,9 +49,8 @@ std::vector<Allocation> OmniAllocator::allocate(
namespace std {

size_t hash<omnimalloc::OmniAllocator>::operator()(
const omnimalloc::OmniAllocator&) const noexcept {
// Stateless class - all instances are equal, use constant hash
return 0x9e3779b9; // arbitrary constant
const omnimalloc::OmniAllocator& allocator) const noexcept {
return hash<std::optional<uint64_t>>{}(allocator.linearize_budget());
}

} // namespace std
19 changes: 15 additions & 4 deletions src/cpp/allocators/omni.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

#pragma once

#include <cstdint>
#include <optional>
#include <vector>

#include "primitives/allocation.hpp"
Expand All @@ -12,17 +14,26 @@ namespace omnimalloc {

// Generalized greedy-portfolio allocator for scalar and vector-clock
// lifetimes: linearizes vector time to surrogate scalars when the
// happens-before order allows (budgeted), otherwise places truthfully on the
// vector conflict graph. Either way the winning first-fit order of the
// 7-order portfolio decides the offsets.
// happens-before order allows (bounded by `linearize_budget`; nullopt means
// unbounded), otherwise places truthfully on the vector conflict graph.
// Either way the winning first-fit order of the 7-order portfolio decides
// the offsets.
class OmniAllocator {
public:
OmniAllocator() = default;
explicit OmniAllocator(std::optional<uint64_t> linearize_budget)
: linearize_budget_(linearize_budget) {}

std::vector<Allocation> allocate(
const std::vector<Allocation>& allocations) const;

[[nodiscard]] std::optional<uint64_t> linearize_budget() const noexcept {
return linearize_budget_;
}

bool operator==(const OmniAllocator&) const noexcept = default;

private:
std::optional<uint64_t> linearize_budget_;
};

} // namespace omnimalloc
Expand Down
1 change: 1 addition & 0 deletions src/cpp/allocators/simulated_annealing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <random>
#include <utility>

#include "common/deadline.hpp"
#include "first_fit.hpp"
#include "local_search.hpp"

Expand Down
19 changes: 10 additions & 9 deletions src/cpp/allocators/simulated_annealing.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,27 @@
#pragma once

#include <cstdint>
#include <optional>
#include <vector>

#include "allocators/defaults.hpp"
#include "primitives/allocation.hpp"

namespace omnimalloc {

// Cooling schedule and iteration budget for `SimulatedAnnealingAllocator`.
// Defaults live in the Python `SimulatedAnnealingConfig` dataclass; every
// field must be set explicitly.
struct SimulatedAnnealingConfig {
uint64_t seed = 42;
int max_iterations = 3000;
uint64_t seed{};
int max_iterations{};
// Percent memory worsening accepted with probability 1/e at iteration 0;
// decays geometrically by `cooling_rate` every iteration.
double initial_temperature = 3.0;
double cooling_rate = 0.998;
// Wall-clock budget checked once per iteration; 0 disables it. Each
double initial_temperature{};
double cooling_rate{};
// Wall-clock budget checked once per iteration; nullopt disables it. Each
// iteration re-evaluates a full O(n) placement, so `max_iterations` alone
// does not bound runtime as `allocations` grows - this does.
double timeout = kDefaultTimeout;
std::optional<double> timeout;
};

// Simulated annealing over first-fit placement orders. Each iteration swaps a
Expand All @@ -35,8 +37,7 @@ struct SimulatedAnnealingConfig {
// equivalent Python-orchestrated local search.
class SimulatedAnnealingAllocator {
public:
explicit SimulatedAnnealingAllocator(
SimulatedAnnealingConfig config = SimulatedAnnealingConfig{});
explicit SimulatedAnnealingAllocator(SimulatedAnnealingConfig config);

[[nodiscard]] std::vector<Allocation> allocate(
const std::vector<Allocation>& allocations) const;
Expand Down
Loading
Loading