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
12 changes: 5 additions & 7 deletions src/python/omnimalloc/analysis/_conflicts.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,11 @@ def conflict_degrees(
) -> list[int]:
"""Conflict count per allocation, aligned with input order.

The degree sequence of the conflict relation behind `conflicts`,
without materializing the adjacency. Positional rather than id-keyed,
so duplicate ids are allowed and counted with multiplicity. Scalar
lifetimes count in O(N log N) without enumerating pairs; vector clocks
take the pairwise C++ sweep (quadratic in the worst case), raising
`RuntimeError` once it exceeds `work_budget` — pass `None` to
always compute.
The degree sequence of the relation behind `conflicts`, without
materializing the adjacency; positional rather than id-keyed, so
duplicate ids are allowed and counted with multiplicity. Scalar
lifetimes count in O(N log N); vector clocks take the quadratic sweep,
raising `RuntimeError` past `work_budget` (`None` always computes).
"""
ensure_valid_budget(work_budget)
return _conflict_degrees(allocations, work_budget)
37 changes: 15 additions & 22 deletions src/python/omnimalloc/analysis/_pressure.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ def closure_pressure(

C++ enumeration of the join-closure of the birth clocks. Pairwise-
concurrent allocations need not share a cut, so this can sit strictly
below `antichain_pressure`; both soundly lower-bound any placement's peak. Raises
`RuntimeError` once the closure exceeds `closure_cap`; pass `None` to
always enumerate — memory then grows with the closure itself.
below `antichain_pressure`; both soundly lower-bound any placement's
peak. Raises `RuntimeError` once the closure exceeds `closure_cap`;
pass `None` to always enumerate — memory then grows with the closure.
"""
ensure_valid_budget(closure_cap, name="closure_cap")
return _closure_pressure(allocations, closure_cap)
Expand All @@ -57,10 +57,9 @@ def closure_pressure(
def placement_pressure(allocations: Sequence[Allocation]) -> int:
"""Peak of a placement: the highest occupied address, max(offset + size).

Simply the pressure the placement realizes after allocation — an upper
bound on `antichain_pressure` (and so on `closure_pressure`), equal to the max
entry of `placement_pressure_per_allocation`. Raises `ValueError` on
unplaced input.
An upper bound on `antichain_pressure` (and so on `closure_pressure`),
equal to the max entry of `placement_pressure_per_allocation`. Raises
`ValueError` on unplaced input.
"""
heights = []
for alloc in allocations:
Expand All @@ -76,15 +75,11 @@ def antichain_pressure_per_allocation(
) -> dict[IdType, int]:
"""Peak pressure over each allocation's own lifetime, keyed by id.

The max-weight antichain through each allocation: the tightest
order-derived lower bound on the pressure any placement can exhibit
while that allocation is live; the max entry equals `antichain_pressure`.
Linearizable instances take one O(N log N) window sweep, genuinely
partial orders solve one pinned antichain (min flow over the conflict
neighborhood) per distinct lifetime — exact, but built to certify
placements, not for the 10k+ hot path. Raises `RuntimeError`
once the linearize attempt or a pinned flow exceeds `work_budget`;
pass `None` to always compute.
`antichain_pressure` restricted to cuts where each allocation is live;
the max entry equals `antichain_pressure`. Genuinely partial orders
solve one pinned antichain per distinct lifetime. Raises
`RuntimeError` once the linearize attempt or a pinned flow exceeds
`work_budget`; pass `None` to always compute.
"""
ensure_valid_budget(work_budget)
ensure_unique_ids(allocations)
Expand All @@ -97,12 +92,10 @@ def closure_pressure_per_allocation(
) -> dict[IdType, int]:
"""Exact realizable peak while each allocation is live, keyed by id.

The max total size at any join-closure cut where the allocation is
live. Can sit elementwise strictly below `antichain_pressure_per_allocation`,
since pairwise-concurrent allocations need not share a cut; the max
entry equals `closure_pressure`. Raises `RuntimeError` once the
closure exceeds `closure_cap`; pass `None` to always enumerate —
memory then grows with the closure itself.
`closure_pressure` restricted to cuts where each allocation is live;
the max entry equals `closure_pressure`, and entries can sit strictly
below `antichain_pressure_per_allocation`. Raises `RuntimeError` once
the closure exceeds `closure_cap`; pass `None` to always enumerate.
"""
ensure_valid_budget(closure_cap, name="closure_cap")
ensure_unique_ids(allocations)
Expand Down
28 changes: 7 additions & 21 deletions src/python/omnimalloc/benchmark/benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,27 +154,13 @@ def run_benchmark(
) -> BenchmarkCampaign:
"""Run a benchmark campaign across multiple allocators and sources.

Args:
allocators: Allocators to benchmark (defaults to all available).
sources: Sources to benchmark (defaults to default source).
variants: For parameterizable sources, specifies allocation counts
(int or tuple of ints). For fixed sources, specifies which
models/pools to test (tuple of names or indices, or int for
"first N", or None for all). A dict keyed by source name
selects variants per source. Examples:
- 100: Test with 100 allocations (parameterizable only)
- (10, 100, 1000): Test with multiple sizes (parameterizable)
- ("model1", "model2"): Test specific models (fixed sources)
- 5: Test first 5 models (fixed sources)
- {"random": (10, 100), "minimalloc": 5}:
per-source variants (sources without an entry use defaults)
- None: Use defaults (all models for fixed, 100 for parameterizable)
campaign_id: Unique identifier for this campaign.
iterations: Number of iterations per variant (for statistical averaging).
validate: Whether to validate allocations after running.

Returns:
BenchmarkCampaign containing all reports.
`allocators` and `sources` default to all available and the default
source. `variants` selects workloads per source: allocation counts
(int or tuple) for parameterizable sources; names, indices, or an int
meaning "first N" for fixed sources; a dict keyed by source name sets
variants per source; `None` uses defaults (all models for fixed, 100
for parameterizable). `iterations` repeats each variant for
statistical averaging; `validate=True` checks every result.
"""
allocators = allocators or available_allocators()
sources = sources or (DEFAULT_SOURCE,)
Expand Down
6 changes: 1 addition & 5 deletions src/python/omnimalloc/benchmark/results/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,7 @@

@dataclass(frozen=True)
class BenchmarkReport:
"""A collection of benchmark results with aggregate statistics.

A report aggregates multiple results (iterations) for the same
allocator/source/variant combination.
"""
"""Aggregates results (iterations) of one allocator/source/variant combination."""

id: IdType
results: tuple[BenchmarkResult, ...]
Expand Down
17 changes: 4 additions & 13 deletions src/python/omnimalloc/benchmark/sources/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,10 @@
class BaseSource(Registered):
"""Base class for benchmark allocation sources with automatic registry.

Registry keys drop the class-role token: RandomSource registers as
"random".

Sources provide workloads at different abstraction levels.
Subclasses must implement `get_allocations()`. Higher-level methods
(pools, memories, systems) have default implementations that build
on allocations.

Sources can be either:
- Parameterizable: Can generate arbitrary numbers of allocations
(e.g., RandomSource)
- Fixed: Have predetermined models/pools with fixed allocation counts
(e.g., Huggingface)
Subclasses implement `get_allocations()`; the pool/memory/system
methods build on it. Sources are either parameterizable (generate any
allocation count, e.g. RandomSource) or fixed (predetermined
models/pools with set counts, e.g. Huggingface).
"""

_strip_suffix: ClassVar[str] = "Source"
Expand Down
6 changes: 1 addition & 5 deletions src/python/omnimalloc/benchmark/sources/huggingface.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,11 +168,7 @@ def _download_onnx_models(


class HuggingfaceSource(BaseSource):
"""Load allocations from Huggingface ONNX models.

This is a fixed source with predetermined models from Huggingface.
Each model becomes a variant that can be benchmarked.
"""
"""Fixed source of Huggingface ONNX model allocations, one variant per model."""

def __init__(
self,
Expand Down
6 changes: 1 addition & 5 deletions src/python/omnimalloc/benchmark/sources/minimalloc.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,7 @@ class MinimallocSubset(str, Enum):


class MinimallocSource(BaseSource):
"""Load allocations from a bundled Minimalloc CSV subset.

This is a fixed source with predetermined pools from the Minimalloc
benchmarks. Pick a bundled ``subset`` to select which pools to load.
"""
"""Fixed source loading pools from a bundled Minimalloc CSV ``subset``."""

def __init__(
self,
Expand Down
14 changes: 4 additions & 10 deletions src/python/omnimalloc/common/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,10 @@
class Registered(ABC):
"""Mixin for auto-registering and managing subclasses.

Any direct subclass of Registered will maintain its own registry. Any
subclass of that subclass that's not abstract will be registered in the
direct subclass's registry.

Registry names derive from the class name: the root's `_strip_suffix`
(its class-role suffix, e.g. "Allocator" or "Source") is stripped from
the end of the name (mid-name occurrences stay), then the remainder
converts to snake_case — the token carries zero information when the
registry is the namespace. Roots leave `_strip_suffix` empty to keep
full class names.
Each direct subclass of Registered maintains its own registry; its
non-abstract descendants register automatically. Registry names strip
the root's `_strip_suffix` (e.g. "Allocator") from the end of the
class name and snake_case the remainder; roots leave it empty.
"""

_registry: ClassVar[dict[str, type[Self]]]
Expand Down
18 changes: 8 additions & 10 deletions src/python/omnimalloc/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,14 @@ def save_allocation(
) -> tuple[Path, ...]:
"""Save the entity's pools to disk as minimalloc-format CSV files.

Saving a `Pool` (or a raw sequence of Allocations, saved as one pool)
writes exactly `path`; a `Memory` or `System` fans out
to one `<stem>_<pool_id>.csv` per pool (the CSV is pool-level minimalloc
interchange). Returns the tuple of paths actually written. Pools with
any placed allocation include an `offset` column (minimalloc's solution
format; unplaced rows leave the cell blank), so `load_allocation`
round-trips placements. Vector-clock lifetimes use an
omnimalloc extension (``:``-joined components, e.g. ``3:0``); such files
round-trip through `load_allocation` but are no longer
minimalloc-readable.
A `Pool` (or raw sequence of Allocations, saved as one pool) writes
exactly `path`; a `Memory` or `System` fans out to one
`<stem>_<pool_id>.csv` per pool. Returns the paths written. Pools
with any placed allocation include an `offset` column (minimalloc's
solution format; unplaced rows blank), so placements round-trip
through `load_allocation`. Vector-clock lifetimes use an omnimalloc
extension (``:``-joined components, e.g. ``3:0``) that round-trips
but is no longer minimalloc-readable.
"""
path_ = Path(path)
path_.parent.mkdir(parents=True, exist_ok=True)
Expand Down
6 changes: 1 addition & 5 deletions src/python/omnimalloc/primitives/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,7 @@

@dataclass(frozen=True)
class Memory:
"""A physical memory unit containing one or more pools.

`size` is the declared memory size (an input); the computed extent of a
placement is `used_size`. `size=None` means unbounded.
"""
"""A physical memory unit containing one or more pools."""

id: IdType
pools: tuple[Pool, ...]
Expand Down
29 changes: 10 additions & 19 deletions src/python/omnimalloc/visualize.py
Original file line number Diff line number Diff line change
Expand Up @@ -558,25 +558,16 @@ def plot_allocation(
) -> None:
"""Plot an allocated entity: `path=None` displays the figure, `path=...` saves it.

Args:
entity: The entity to plot (System, Memory, Pool, or a raw sequence
of Allocations, plotted as a single pool).
path: Where to save the figure; `None` displays it instead.
capacities: Extra capacity lines to draw, keyed by label then by
memory id (byte limits, drawn as horizontal lines).
view: "panel" draws each memory once over a happens-before-monotone
virtual time (exact for scalar or linearizable lifetimes, else
a sound clock-component-sum projection annotated with its
conflict coverage on modestly sized memories)
"lanes" draws one subplot per thread showing its local-time
projection. Both views only ever show genuine conflicts as
temporal overlaps; scalar entities render identically under
either.
max_lanes: Lanes view only (rejected under other views): cap each
memory to its top-k threads by peak definitely-live
occupancy.

Raises `ImportError` without matplotlib.
Accepts a System, Memory, Pool, or raw sequence of Allocations
(plotted as a single pool). `capacities` draws extra horizontal limit
lines, keyed by label then memory id. `view="panel"` draws each
memory once over a happens-before-monotone virtual time (exact for
scalar or linearizable lifetimes, else a sound projection annotated
with its conflict coverage); `view="lanes"` draws one subplot per
thread's local-time projection, capped to the top `max_lanes` threads
by peak definitely-live occupancy. Both views only ever show genuine
conflicts as temporal overlaps. Raises `ImportError` without
matplotlib.
"""
if view not in ("panel", "lanes"):
raise ValueError(f'view must be "panel" or "lanes", got {view!r}')
Expand Down
Loading