122 lines
5.1 KiB
Python
122 lines
5.1 KiB
Python
"""Tests that estimate() normalizes arbitrary-order node/core grids (issue #11)."""
|
|
|
|
from nix_estimator.estimate import estimate
|
|
|
|
|
|
def _toy():
|
|
"""A root fanning out to heavy leaves, joined at a sink.
|
|
|
|
Wide enough that node count matters, heavy enough that cores matter.
|
|
Durations are pinned via ``history`` so the grid is deterministic.
|
|
"""
|
|
|
|
def p(name):
|
|
return f"/nix/store/{'0' * 32}-{name}.drv"
|
|
|
|
leaves = [p(f"heavylib{i}") for i in range(4)]
|
|
root, sink = p("root"), p("sink")
|
|
closure = {d: {} for d in [root, *leaves, sink]}
|
|
preds = {root: [], sink: leaves, **{leaf: [root] for leaf in leaves}}
|
|
nodes = set(closure)
|
|
history = {"root": 1.0, "sink": 1.0, **{f"heavylib{i}": 20.0 for i in range(4)}}
|
|
return closure, preds, nodes, history
|
|
|
|
|
|
def test_unsorted_core_grid_matches_sorted():
|
|
closure, preds, nodes, history = _toy()
|
|
ref = estimate(closure, preds, nodes, history=history, core_grid=(8, 16, 32))
|
|
shuf = estimate(closure, preds, nodes, history=history, core_grid=(32, 8, 16))
|
|
assert shuf.recommendation == ref.recommendation
|
|
# normalization must not silently swallow the top core count
|
|
assert (
|
|
shuf.recommendation["one_big_node_min"]
|
|
== ref.recommendation["one_big_node_min"]
|
|
)
|
|
|
|
|
|
def test_unsorted_node_grid_matches_sorted():
|
|
closure, preds, nodes, history = _toy()
|
|
ref = estimate(closure, preds, nodes, history=history, node_grid=(1, 2, 4, 8))
|
|
shuf = estimate(closure, preds, nodes, history=history, node_grid=(8, 1, 4, 2))
|
|
assert shuf.recommendation == ref.recommendation
|
|
|
|
|
|
def test_unsorted_grids_duplicates_deduped():
|
|
closure, preds, nodes, history = _toy()
|
|
ref = estimate(closure, preds, nodes, history=history, core_grid=(8, 16))
|
|
dup = estimate(closure, preds, nodes, history=history, core_grid=(16, 8, 8, 16))
|
|
assert dup.recommendation == ref.recommendation
|
|
|
|
|
|
def test_recommend_node_grid_min_above_peak():
|
|
# Issue #18: if every node_grid value exceeds the graph's peak concurrency,
|
|
# the clamp target (peak) is below all grid keys. _recommend must snap back
|
|
# onto an existing grid value instead of KeyError-ing on at[peak].
|
|
def p(name):
|
|
return f"/nix/store/{'0' * 32}-{name}.drv"
|
|
|
|
# linear chain: peak concurrency == 1, well below any node_grid entry.
|
|
a, b, c = p("a"), p("b"), p("c")
|
|
closure = {a: {}, b: {}, c: {}}
|
|
preds = {a: [], b: [a], c: [b]}
|
|
nodes = set(closure)
|
|
history = {"a": 1.0, "b": 1.0, "c": 1.0}
|
|
est = estimate(closure, preds, nodes, history=history, node_grid=(4, 8, 16))
|
|
assert est.peak_parallelism == 1
|
|
# snapped down to the smallest available grid value
|
|
assert est.recommendation["nodes"] == 4
|
|
assert "est_makespan_min" in est.recommendation
|
|
|
|
|
|
def test_estimate_max_jobs_helps_wide_small_graph():
|
|
# Issue #13: a fan of many small, weakly-parallel derivations. max_jobs>1
|
|
# packs several per node; the per-job core split barely hurts small drvs, so
|
|
# the single-node makespan drops.
|
|
def p(name):
|
|
return f"/nix/store/{'0' * 32}-{name}.drv"
|
|
|
|
leaves = [p(f"small{i}") for i in range(12)]
|
|
root, sink = p("root"), p("sink")
|
|
closure = {d: {} for d in [root, *leaves, sink]}
|
|
preds = {root: [], sink: leaves, **{leaf: [root] for leaf in leaves}}
|
|
nodes = set(closure)
|
|
history = {"root": 0.5, "sink": 0.5, **{f"small{i}": 2.0 for i in range(12)}}
|
|
base = estimate(closure, preds, nodes, history=history, max_jobs=1)
|
|
packed = estimate(closure, preds, nodes, history=history, max_jobs=4)
|
|
assert packed.max_jobs == 4
|
|
# single node, 8 cores: 12 small builds serialize at mj=1 but pack at mj=4
|
|
assert packed.grid[(1, 8)] < base.grid[(1, 8)]
|
|
|
|
|
|
def test_estimate_ram_budget_lengthens_makespan():
|
|
# Issue #15: many RAM-heavy builds (ghc ~6 GB each) on nodes with a tight
|
|
# RAM budget run fewer-at-a-time than max_jobs allows -> longer makespan than
|
|
# the unconstrained run at the same (nodes, cores, max_jobs).
|
|
def p(name):
|
|
return f"/nix/store/{'0' * 32}-{name}.drv"
|
|
|
|
leaves = [p(f"ghc-{i}") for i in range(6)]
|
|
root = p("root")
|
|
closure = {d: {} for d in [root, *leaves]}
|
|
preds = {root: [], **{leaf: [root] for leaf in leaves}}
|
|
nodes = set(closure)
|
|
history = {"root": 0.5} # leaves fall through to the ghc heuristic (RAM ~6GB)
|
|
unc = estimate(closure, preds, nodes, history=history, max_jobs=4)
|
|
capped = estimate(
|
|
closure, preds, nodes, history=history, max_jobs=4, node_ram_gb=8.0
|
|
)
|
|
assert capped.node_ram_gb == 8.0
|
|
# 8 GB fits only one 6-GB ghc build per node at a time, so a single node is
|
|
# slower under the RAM cap than when 4 could pack.
|
|
assert capped.grid[(1, 8)] > unc.grid[(1, 8)]
|
|
|
|
|
|
def test_knee_picks_biggest_core_that_helps():
|
|
# one heavy single-threaded-ish pole: recommendation stays sane and the
|
|
# reported one-big-node uses the largest core count regardless of input order.
|
|
closure, preds, nodes, history = _toy()
|
|
est = estimate(closure, preds, nodes, history=history, core_grid=(32, 16, 8))
|
|
assert est.recommendation["cores_per_node"] in (8, 16, 32)
|
|
# node choice never exceeds the graph width (4 heavy leaves)
|
|
assert est.recommendation["nodes"] <= 4
|