66import uuid
77from datetime import datetime
88from functools import lru_cache
9- from typing import Callable , Optional , Tuple
9+ from typing import Any , Callable , Dict , Optional , Tuple
1010
1111import networkx as nx
1212import zarr
1616from cubed .primitive .types import PrimitiveOperation
1717from cubed .runtime .pipeline import visit_nodes
1818from 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
2020from cubed .utils import (
2121 chunk_memory ,
2222 extract_stack_summaries ,
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
2934CONTEXT_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
0 commit comments