Skip to content

Commit f2c0ae3

Browse files
authored
Simplify resume implementation by taking out of runtime (it's an array-level feature) (#725)
1 parent 34b658a commit f2c0ae3

16 files changed

Lines changed: 73 additions & 91 deletions

File tree

cubed/core/plan.py

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import uuid
77
from datetime import datetime
88
from functools import lru_cache
9-
from typing import Callable, Optional, Tuple
9+
from typing import Any, Callable, Dict, Optional, Tuple
1010

1111
import networkx as nx
1212
import zarr
@@ -16,7 +16,7 @@
1616
from cubed.primitive.types import PrimitiveOperation
1717
from cubed.runtime.pipeline import visit_nodes
1818
from cubed.runtime.types import ComputeEndEvent, ComputeStartEvent, CubedPipeline
19-
from cubed.storage.zarr import LazyZarrArray
19+
from cubed.storage.zarr import LazyZarrArray, open_if_lazy_zarr_array
2020
from cubed.utils import (
2121
chunk_memory,
2222
extract_stack_summaries,
@@ -25,6 +25,11 @@
2525
memory_repr,
2626
)
2727

28+
try:
29+
from zarr.errors import ArrayNotFoundError # type: ignore
30+
except ImportError:
31+
ArrayNotFoundError = FileNotFoundError # zarr-python 3
32+
2833
# A unique ID with sensible ordering, used for making directory names
2934
CONTEXT_ID = f"cubed-{datetime.now().strftime('%Y%m%dT%H%M%S')}-{uuid.uuid4()}"
3035

@@ -297,16 +302,22 @@ def execute(
297302
)
298303
dag = finalized_plan.dag
299304

305+
if resume:
306+
# mark nodes as computed so they are not visited by visit_nodes
307+
dag = dag.copy()
308+
nodes = {n: d for (n, d) in dag.nodes(data=True)}
309+
for name in list(nx.topological_sort(dag)):
310+
dag.nodes[name]["computed"] = already_computed(name, dag, nodes)
311+
300312
compute_id = f"compute-{datetime.now().strftime('%Y%m%dT%H%M%S.%f')}"
301313

302314
if callbacks is not None:
303-
event = ComputeStartEvent(compute_id, dag, resume)
315+
event = ComputeStartEvent(compute_id, dag)
304316
[callback.on_compute_start(event) for callback in callbacks]
305317
executor.execute_dag(
306318
dag,
307319
compute_id=compute_id,
308320
callbacks=callbacks,
309-
resume=resume,
310321
spec=spec,
311322
**kwargs,
312323
)
@@ -498,11 +509,10 @@ class FinalizedPlan:
498509
def __init__(self, dag):
499510
self.dag = dag
500511

501-
def max_projected_mem(self, resume=None):
512+
def max_projected_mem(self):
502513
"""Return the maximum projected memory across all tasks to execute this plan."""
503514
projected_mem_values = [
504-
node["primitive_op"].projected_mem
505-
for _, node in visit_nodes(self.dag, resume=resume)
515+
node["primitive_op"].projected_mem for _, node in visit_nodes(self.dag)
506516
]
507517
return max(projected_mem_values) if len(projected_mem_values) > 0 else 0
508518

@@ -514,10 +524,10 @@ def num_primitive_ops(self) -> int:
514524
"""Return the number of primitive operations in this plan."""
515525
return len(list(visit_nodes(self.dag)))
516526

517-
def num_tasks(self, resume=None):
527+
def num_tasks(self):
518528
"""Return the number of tasks needed to execute this plan."""
519529
tasks = 0
520-
for _, node in visit_nodes(self.dag, resume=resume):
530+
for _, node in visit_nodes(self.dag):
521531
tasks += node["primitive_op"].num_tasks
522532
return tasks
523533

@@ -589,3 +599,35 @@ def create_zarr_arrays(lazy_zarr_arrays, allowed_mem, reserved_mem):
589599
num_tasks=num_tasks,
590600
fusable_with_predecessors=False,
591601
)
602+
603+
604+
def already_computed(name, dag, nodes: Dict[str, Any]) -> bool:
605+
"""
606+
Return True if the array for a node doesn't have a pipeline to compute it,
607+
or if all its outputs have already been computed (all chunks are present).
608+
"""
609+
pipeline = nodes[name].get("pipeline", None)
610+
if pipeline is None:
611+
return True
612+
613+
# if no outputs have targets then need to compute (this is the create-arrays case)
614+
if all(
615+
[nodes[output].get("target", None) is None for output in dag.successors(name)]
616+
):
617+
return False
618+
619+
for output in dag.successors(name):
620+
target = nodes[output].get("target", None)
621+
if target is not None:
622+
try:
623+
target = open_if_lazy_zarr_array(target)
624+
if not hasattr(target, "nchunks_initialized"):
625+
raise NotImplementedError(
626+
f"Zarr array type {type(target)} does not support resume since it doesn't have a 'nchunks_initialized' property"
627+
)
628+
# this check can be expensive since it has to list the directory to find nchunks_initialized
629+
if target.ndim == 0 or target.nchunks_initialized != target.nchunks:
630+
return False
631+
except ArrayNotFoundError:
632+
return False
633+
return True

cubed/diagnostics/history.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
class HistoryCallback(Callback):
1111
def on_compute_start(self, event):
1212
plan = []
13-
for name, node in visit_nodes(event.dag, event.resume):
13+
for name, node in visit_nodes(event.dag):
1414
primitive_op = node["primitive_op"]
1515
plan.append(
1616
dict(

cubed/diagnostics/mem_warn.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class MemoryWarningCallback(Callback):
99
def on_compute_start(self, event):
1010
# store ops keyed by name
1111
self.ops = {}
12-
for name, node in visit_nodes(event.dag, event.resume):
12+
for name, node in visit_nodes(event.dag):
1313
primitive_op = node["primitive_op"]
1414
self.ops[name] = primitive_op
1515

cubed/diagnostics/rich.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def on_compute_start(self, event):
4242
progress = logger_aware_progress.__enter__()
4343

4444
progress_tasks = {}
45-
for name, node in visit_nodes(event.dag, event.resume):
45+
for name, node in visit_nodes(event.dag):
4646
num_tasks = node["primitive_op"].num_tasks
4747
op_display_name = node["op_display_name"].replace("\n", " ")
4848
progress_task = progress.add_task(

cubed/diagnostics/tqdm.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ def on_compute_start(self, event):
2121
max_op_display_name = (
2222
max(
2323
len(node["op_display_name"].replace("\n", " "))
24-
for _, node in visit_nodes(event.dag, event.resume)
24+
for _, node in visit_nodes(event.dag)
2525
)
2626
+ 1 # for the colon
2727
)
2828

2929
self.pbars = {}
30-
for i, (name, node) in enumerate(visit_nodes(event.dag, event.resume)):
30+
for i, (name, node) in enumerate(visit_nodes(event.dag)):
3131
num_tasks = node["primitive_op"].num_tasks
3232
op_display_name = node["op_display_name"].replace("\n", " ") + ":"
3333
# note double curlies to get literal { and } for tqdm bar format

cubed/runtime/asyncio.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,6 @@ async def async_map_dag(
126126
create_futures_func: Callable,
127127
dag: MultiDiGraph,
128128
callbacks: Optional[Sequence[Callback]] = None,
129-
resume: Optional[bool] = None,
130129
compute_arrays_in_parallel: Optional[bool] = None,
131130
**kwargs,
132131
) -> None:
@@ -135,7 +134,7 @@ async def async_map_dag(
135134
"""
136135
if not compute_arrays_in_parallel:
137136
# run one pipeline at a time
138-
for name, node in visit_nodes(dag, resume=resume):
137+
for name, node in visit_nodes(dag):
139138
handle_operation_start_callbacks(callbacks, name)
140139
st = pipeline_to_stream(
141140
create_futures_func, name, node["pipeline"], **kwargs
@@ -144,7 +143,7 @@ async def async_map_dag(
144143
async for result, stats in streamer:
145144
handle_callbacks(callbacks, result, stats)
146145
else:
147-
for gen in visit_node_generations(dag, resume=resume):
146+
for gen in visit_node_generations(dag):
148147
# run pipelines in the same topological generation in parallel by merging their streams
149148
streams = [
150149
pipeline_to_stream(

cubed/runtime/executors/beam.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,11 @@ class BeamExecutor(DagExecutor):
8686
def name(self) -> str:
8787
return "beam"
8888

89-
def execute_dag(
90-
self, dag, callbacks=None, resume=None, spec=None, compute_id=None, **kwargs
91-
):
89+
def execute_dag(self, dag, callbacks=None, spec=None, compute_id=None, **kwargs):
9290
dag = dag.copy()
9391
pipeline = beam.Pipeline(**kwargs)
9492

95-
for name, node in visit_nodes(dag, resume=resume):
93+
for name, node in visit_nodes(dag):
9694
cubed_pipeline = node["pipeline"]
9795

9896
inputs = list(dag.predecessors(name))

cubed/runtime/executors/coiled.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,14 @@ def execute_dag(
3131
self,
3232
dag: MultiDiGraph,
3333
callbacks: Optional[Sequence[Callback]] = None,
34-
resume: Optional[bool] = None,
3534
spec: Optional[Spec] = None,
3635
compute_id: Optional[str] = None,
3736
**coiled_kwargs: Mapping[str, Any],
3837
) -> None:
3938
merged_kwargs = {**self.kwargs, **coiled_kwargs}
4039
minimum_workers = merged_kwargs.pop("minimum_workers", None)
4140
# Note this currently only builds the task graph for each stage once it gets to that stage in computation
42-
for name, node in visit_nodes(dag, resume=resume):
41+
for name, node in visit_nodes(dag):
4342
handle_operation_start_callbacks(callbacks, name)
4443
pipeline = node["pipeline"]
4544
coiled_function = make_coiled_function(pipeline.function, merged_kwargs)

cubed/runtime/executors/dask.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ def execute_dag(
6969
self,
7070
dag: MultiDiGraph,
7171
callbacks: Optional[Sequence[Callback]] = None,
72-
resume: Optional[bool] = None,
7372
spec: Optional[Spec] = None,
7473
compute_id: Optional[str] = None,
7574
compute_kwargs: Optional[Dict[str, Any]] = None,
@@ -80,7 +79,6 @@ def execute_dag(
8079
self._async_execute_dag(
8180
dag,
8281
callbacks=callbacks,
83-
resume=resume,
8482
spec=spec,
8583
compute_kwargs=compute_kwargs,
8684
compute_id=compute_id,
@@ -92,7 +90,6 @@ async def _async_execute_dag(
9290
self,
9391
dag: MultiDiGraph,
9492
callbacks: Optional[Sequence[Callback]] = None,
95-
resume: Optional[bool] = None,
9693
spec: Optional[Spec] = None,
9794
compute_kwargs: Optional[Dict[str, Any]] = None,
9895
compute_arrays_in_parallel: Optional[bool] = None,
@@ -118,7 +115,6 @@ async def _async_execute_dag(
118115
create_futures_func,
119116
dag=dag,
120117
callbacks=callbacks,
121-
resume=resume,
122118
compute_arrays_in_parallel=compute_arrays_in_parallel,
123119
create_backup_futures_func=create_backup_futures_func,
124120
**kwargs,

cubed/runtime/executors/lithops.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,6 @@ def map_unordered(
167167
def execute_dag(
168168
dag: MultiDiGraph,
169169
callbacks: Optional[Sequence[Callback]] = None,
170-
resume: Optional[bool] = None,
171170
spec: Optional[Spec] = None,
172171
compute_arrays_in_parallel: Optional[bool] = None,
173172
**kwargs,
@@ -188,7 +187,7 @@ def execute_dag(
188187
)
189188
with RetryingFunctionExecutor(function_executor) as executor:
190189
if not compute_arrays_in_parallel:
191-
for name, node in visit_nodes(dag, resume=resume):
190+
for name, node in visit_nodes(dag):
192191
handle_operation_start_callbacks(callbacks, name)
193192
pipeline = node["pipeline"]
194193
for result, stats in map_unordered(
@@ -207,7 +206,7 @@ def execute_dag(
207206
):
208207
handle_callbacks(callbacks, result, stats)
209208
else:
210-
for gen in visit_node_generations(dag, resume=resume):
209+
for gen in visit_node_generations(dag):
211210
group_map_functions = []
212211
group_map_iterdata = []
213212
group_names = []
@@ -262,7 +261,6 @@ def execute_dag(
262261
self,
263262
dag: MultiDiGraph,
264263
callbacks: Optional[Sequence[Callback]] = None,
265-
resume: Optional[bool] = None,
266264
spec: Optional[Spec] = None,
267265
compute_id: Optional[str] = None,
268266
**kwargs,
@@ -271,7 +269,6 @@ def execute_dag(
271269
execute_dag(
272270
dag,
273271
callbacks=callbacks,
274-
resume=resume,
275272
spec=spec,
276273
compute_id=compute_id,
277274
**merged_kwargs,

0 commit comments

Comments
 (0)