Skip to content

Commit e497e81

Browse files
authored
feat(patching): give the read-ahead up before refusing a sweep
The block a sweep holds is mostly what the chain must hold to run at all: the source window it pulls, the rows it lands, the buffers its widest stage allocates. The read-ahead queue is the one part the sizing CHOSE rather than met, and what it buys is wall clock -- half a second of a 6.7 s run, by _sweep_depth's own measurement. A sweep about to refuse has no clock to buy. So it stops buying it. Three resident source regions become one and the search runs once more; only if that does not fit either does it refuse, now naming both figures so a reader knows which one to clear. On a chain whose stage buffers dominate the band is narrow, 1.31x to 1.34x on the tiling fixture; on a pointwise one it is 2.5x. Inside the band the alternative was not a slower run but no run. _sweep_depth had to learn to go down before it goes up: a tile found without the queue, swept with one, would hold what the sizing was never told about -- the budget's promise lost to a default nobody revisited. And _keeps_the_block now asks the search rather than _sweep_tile, which falls back: a depth that cannot afford the block would otherwise come back holding it, and every depth would look affordable. The refusal test's own budget stopped refusing, which is the feature: half of one row's queued price is bought by the serial retry. It is now set under the serial price, and asserts both figures are named.
1 parent 787fb60 commit e497e81

2 files changed

Lines changed: 93 additions & 22 deletions

File tree

konfai/data/patching.py

Lines changed: 58 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3209,19 +3209,27 @@ def _sweep_depth(
32093209
second block in flight recovers 0.5 s of a 6.7 s run and a third recovers none.
32103210
"""
32113211
depth = _sweep_pipeline_depth()
3212+
# DOWN BEFORE UP. `tile` may be the one the sizing found only after giving the queue up
3213+
# (:meth:`_sweep_tile`), and a run that kept the queue anyway would hold what the sizing was
3214+
# never told about -- the budget's whole promise, lost to a default nobody revisited.
3215+
while depth and not self._keeps_the_block(spatial, channels, plans, tile, depth):
3216+
depth -= 1
32123217
while depth and depth < _SWEEP_MAX_DEPTH and self._keeps_the_block(spatial, channels, plans, tile, depth + 1):
32133218
depth += 1
32143219
return depth
32153220

32163221
def _keeps_the_block(
32173222
self, spatial: list[int], channels: int, plans: Sequence["_ReadStagePlan"], tile: list[int], depth: int
32183223
) -> bool:
3219-
"""Whether a queue of ``depth`` still leaves the sweep exactly ``tile``: a deeper one the
3220-
budget cannot hold refuses, and a refusal here is the answer no, not the sweep's failure."""
3221-
try:
3222-
return self._sweep_tile(spatial, channels, plans, depth) == tile
3223-
except DatasetManagerError:
3224-
return False
3224+
"""Whether a queue of ``depth`` both affords ``tile`` and still picks it.
3225+
3226+
Asked of the search and not of :meth:`_sweep_tile`, which falls back to no queue at all: a
3227+
depth that cannot hold the block would come back holding it, and every depth would look
3228+
affordable.
3229+
"""
3230+
budget = self._sweep_budget_bytes
3231+
found, held = self._tile_within(spatial, channels, plans, depth, budget)
3232+
return found == tile and (not budget or budget <= 0 or held <= budget)
32253233

32263234
def read_granularity(self) -> tuple[int, ...] | None:
32273235
"""The stored block this case's source reads are served in, spatial axes only, or ``None``
@@ -3521,24 +3529,54 @@ def _sweep_tile(
35213529
search is over the height, because that is the one free parameter of the decomposition.
35223530
"""
35233531
depth = _sweep_pipeline_depth() if depth is None else depth
3524-
cap = self._sweep_rows(spatial, channels, plans, depth)
35253532
budget = self._sweep_budget_bytes
3533+
tile, held = self._tile_within(spatial, channels, plans, depth, budget)
3534+
if not budget or budget <= 0 or held <= budget:
3535+
return tile
3536+
# THE READ-AHEAD IS THE ONE PART OF THE PRICE THE SIZING CHOSE. Everything else in the block
3537+
# is what the chain must hold to run at all; the queue is bought, and what it buys is wall
3538+
# clock (_sweep_depth: half a second of a 6.7 s run). A sweep about to refuse has no clock to
3539+
# buy, so it gives the queue up and asks once more. Three source regions resident become one,
3540+
# which is a quarter to a third of the block on a chain whose stage buffers dominate -- a
3541+
# narrow band, and inside it the difference is running against not running.
3542+
serial = None
3543+
if depth > 0:
3544+
candidate, serial = self._tile_within(spatial, channels, plans, 0, budget)
3545+
if serial <= budget:
3546+
return candidate
3547+
raise DatasetManagerError(
3548+
f"'{self.name}': no region of '{self.group_src}' fits the per-rank memory budget"
3549+
f" ({format_bytes(budget)}): the smallest one this chain can sweep holds"
3550+
f" {format_bytes(held)}"
3551+
+ (f", and {format_bytes(serial)} with the read-ahead given up" if serial is not None else "")
3552+
+ ".",
3553+
"Raise 'memory_budget'.",
3554+
)
3555+
3556+
def _tile_within(
3557+
self,
3558+
spatial: list[int],
3559+
channels: int,
3560+
plans: Sequence["_ReadStagePlan"],
3561+
depth: int,
3562+
budget: float | None,
3563+
) -> tuple[list[int], int]:
3564+
"""The best block a sweep of ``depth`` can afford, and what it holds: the search alone.
3565+
3566+
No refusal and no fallback, because two callers ask it two different questions -- whether a
3567+
deeper queue still buys the same block (:meth:`_keeps_the_block`) and what to do when none
3568+
of them fits (:meth:`_sweep_tile`) -- and a search that answered either for them would
3569+
answer the other one wrong.
3570+
"""
3571+
cap = self._sweep_rows(spatial, channels, plans, depth)
35263572
if not budget or budget <= 0:
3527-
return self._sweep_shape(spatial, plans, cap)
3528-
# The bisection never takes one row as affordable: the refusal below answers for it. What it
3529-
# finds is then judged against the store's own heights, because the price steps rather than
3530-
# climbs and bisection lands somewhere affordable, not on the best region the budget buys.
3573+
return self._sweep_shape(spatial, plans, cap), 0
3574+
# The bisection never takes one row as affordable: the caller answers for it. What it finds
3575+
# is then judged against the store's own heights, because the price steps rather than climbs
3576+
# and bisection lands somewhere affordable, not on the best region the budget buys.
35313577
low = self._rows_within(spatial, channels, plans, depth, budget, cap)
35323578
tile = self._best_tile(spatial, channels, plans, depth, budget, [low, *self._grid_rows(cap)])
3533-
held = self.sweep_block_bytes(spatial, channels, plans, tile, depth)
3534-
if held > budget:
3535-
raise DatasetManagerError(
3536-
f"'{self.name}': no region of '{self.group_src}' fits the per-rank memory budget"
3537-
f" ({format_bytes(budget)}): the smallest one this chain can sweep holds"
3538-
f" {format_bytes(held)}.",
3539-
"Raise 'memory_budget'.",
3540-
)
3541-
return tile
3579+
return tile, self.sweep_block_bytes(spatial, channels, plans, tile, depth)
35423580

35433581
def _get_streamed_data(
35443582
self,

tests/unit/test_sweep_tiling.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,15 +253,48 @@ def test_a_regrid_pays_for_what_it_pulls_and_not_for_what_it_lands(tmp_path: Pat
253253
assert _block_voxels(regrid, plans) < _block_voxels(pointwise, ())
254254

255255

256+
def test_a_budget_that_only_fits_without_the_queue_gives_the_queue_up(tmp_path: Path) -> None:
257+
"""The read-ahead is bought, not owed. A sweep that cannot afford it stops buying it.
258+
259+
Three source regions are resident with a queue and one without, so a chain whose stage buffers
260+
do not dominate holds a quarter to a third less serially. That band is narrow -- 1.31x to 1.34x
261+
on this fixture -- and inside it the alternative is not a slower run but no run at all.
262+
"""
263+
source, _volume = _sheared_fixture(tmp_path)
264+
resample = Resample(reference="TARGET", reference_group="GRID", reference_dataset=f"{tmp_path / 'ref'}:h5")
265+
manager = _manager(source, [resample, Save(f"{tmp_path / 'out'}:h5")])
266+
plans = _sweep_plans(manager)
267+
base = _sweep_pipeline_depth()
268+
assert base > 0, "a rank with one core queues nothing and has nothing to give up"
269+
270+
tile = manager._sweep_shape(list(LANDING), plans, 1)
271+
queued = manager.sweep_block_bytes(list(LANDING), 1, plans, tile, base)
272+
serial = manager.sweep_block_bytes(list(LANDING), 1, plans, tile, 0)
273+
assert serial < queued, "the queue is part of what one row costs"
274+
275+
# A budget between the two: the queue is what makes it refuse, and nothing else does.
276+
manager.set_memory_budget((queued + serial) / 2.0)
277+
found = manager._sweep_tile(list(LANDING), 1, plans)
278+
assert manager.sweep_block_bytes(list(LANDING), 1, plans, found, 0) <= manager._sweep_budget_bytes
279+
280+
# And the run walks the depth the sizing solved for, or it holds what it was never priced for.
281+
assert manager._sweep_depth(list(LANDING), 1, plans, found) == 0
282+
283+
256284
def test_a_budget_no_region_fits_refuses_with_both_figures(tmp_path: Path) -> None:
257285
"""A budget one row of the landing does not fit is not a one-row sweep: it is a refusal naming
258286
the budget and what the smallest region holds, so the reader knows what to raise it to."""
259287
source, _volume = _sheared_fixture(tmp_path)
260288
manager = _manager(source, [Save(f"{tmp_path / 'out'}:h5")])
261-
manager.set_memory_budget(_priced(manager, (), 1) / 2.0)
289+
# Under what one row holds WITHOUT the queue, since that is the last thing the sizing tries:
290+
# half of the queued price is a budget the serial retry now buys, and buying it is the point.
291+
serial = manager.sweep_block_bytes(list(LANDING), 1, (), manager._sweep_shape(list(LANDING), (), 1), 0)
292+
manager.set_memory_budget(serial / 2.0)
262293

263-
with pytest.raises(DatasetManagerError, match=r"no region of 'CT' fits the per-rank memory budget"):
294+
with pytest.raises(DatasetManagerError, match=r"no region of 'CT' fits the per-rank memory budget") as raised:
264295
manager._sweep_tile(list(LANDING), 1)
296+
# Both attempts, so the reader raising the budget knows which figure to clear.
297+
assert "with the read-ahead given up" in str(raised.value)
265298
assert manager.stream_refusal(0) is not None, "and the plan routes the case away from streaming"
266299

267300

0 commit comments

Comments
 (0)