Skip to content

Commit 71194a4

Browse files
authored
add run_all_sub_nodes for map tasks (#3399)
* add run_all_sub_nodes for map tasks Signed-off-by: Paul Dittamo <pvdittamo@gmail.com> * update unit tests Signed-off-by: Paul Dittamo <pvdittamo@gmail.com> --------- Signed-off-by: Paul Dittamo <pvdittamo@gmail.com>
1 parent 6297a98 commit 71194a4

6 files changed

Lines changed: 142 additions & 1 deletion

File tree

flytekit/core/array_node.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def __init__(
4141
min_successes: Optional[int] = None,
4242
min_success_ratio: Optional[float] = None,
4343
metadata: Optional[_workflow_model.NodeMetadata] = None,
44+
run_all_sub_nodes: bool = False,
4445
):
4546
"""
4647
:param target: The target Flyte entity to map over
@@ -52,11 +53,13 @@ def __init__(
5253
min_success_ratio
5354
:param min_success_ratio: The minimum ratio of successful executions.
5455
:param metadata: The metadata for the underlying node
56+
:param run_all_sub_nodes: If True, all sub-nodes will run to completion even after the failure threshold is met
5557
"""
5658
from flytekit.remote import FlyteLaunchPlan
5759

5860
self.target = target
5961
self._concurrency = concurrency
62+
self._run_all_sub_nodes = run_all_sub_nodes
6063
self.id = target.name
6164
self._bindings = bindings or []
6265
self.metadata = metadata
@@ -226,6 +229,10 @@ def min_successes(self) -> Optional[int]:
226229
def concurrency(self) -> Optional[int]:
227230
return self._concurrency
228231

232+
@property
233+
def run_all_sub_nodes(self) -> bool:
234+
return self._run_all_sub_nodes
235+
229236
@property
230237
def execution_mode(self) -> _core_workflow.ArrayNode.ExecutionMode:
231238
return self._execution_mode
@@ -275,6 +282,7 @@ def array_node(
275282
concurrency: Optional[int] = None,
276283
min_success_ratio: Optional[float] = None,
277284
min_successes: Optional[int] = None,
285+
run_all_sub_nodes: bool = False,
278286
):
279287
"""
280288
ArrayNode implementation that maps over tasks and other Flyte entities
@@ -287,6 +295,7 @@ def array_node(
287295
:param min_successes: The minimum number of successful executions. If set, this takes precedence over
288296
min_success_ratio
289297
:param min_success_ratio: The minimum ratio of successful executions
298+
:param run_all_sub_nodes: If True, all sub-nodes will run to completion even after the failure threshold is met
290299
:return: A callable function that takes in keyword arguments and returns a Promise created by
291300
flyte_entity_call_handler
292301
"""
@@ -300,6 +309,7 @@ def array_node(
300309
concurrency=concurrency,
301310
min_successes=min_successes,
302311
min_success_ratio=min_success_ratio,
312+
run_all_sub_nodes=run_all_sub_nodes,
303313
)
304314

305315
return node

flytekit/core/array_node_map_task.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def __init__(
4242
min_success_ratio: Optional[float] = None,
4343
bound_inputs: Optional[Set[str]] = None,
4444
bound_inputs_values: Optional[Dict[str, Any]] = None,
45+
run_all_sub_nodes: bool = False,
4546
**kwargs,
4647
):
4748
"""
@@ -51,6 +52,7 @@ def __init__(
5152
:param min_success_ratio: The minimum ratio of successful executions
5253
:param bound_inputs: The set of inputs that should be bound to the map task
5354
:param bound_inputs_values: Inputs that are bound to the array node and will not be mapped over
55+
:param run_all_sub_nodes: If True, all sub-nodes will run to completion even after the failure threshold is met
5456
:param kwargs: Additional keyword arguments to pass to the base class
5557
"""
5658
self._partial = None
@@ -113,6 +115,7 @@ def __init__(
113115
self._concurrency: Optional[int] = concurrency
114116
self._min_successes: Optional[int] = min_successes
115117
self._min_success_ratio: Optional[float] = min_success_ratio
118+
self._run_all_sub_nodes: bool = run_all_sub_nodes
116119
self._collection_interface = collection_interface
117120

118121
self._execution_mode: _core_workflow.ArrayNode.ExecutionMode = _core_workflow.ArrayNode.FULL_STATE
@@ -168,6 +171,10 @@ def min_successes(self) -> Optional[int]:
168171
def concurrency(self) -> Optional[int]:
169172
return self._concurrency
170173

174+
@property
175+
def run_all_sub_nodes(self) -> bool:
176+
return self._run_all_sub_nodes
177+
171178
@property
172179
def python_function_task(self) -> Union[PythonFunctionTask, PythonInstanceTask]:
173180
return self._run_task
@@ -385,6 +392,7 @@ def map_task(
385392
concurrency: Optional[int] = None,
386393
min_successes: Optional[int] = None,
387394
min_success_ratio: float = 1.0,
395+
run_all_sub_nodes: bool = False,
388396
**kwargs,
389397
):
390398
"""
@@ -398,6 +406,7 @@ def map_task(
398406
array node will inherit parallelism from the workflow
399407
:param min_successes: The minimum number of successful executions
400408
:param min_success_ratio: The minimum ratio of successful executions
409+
:param run_all_sub_nodes: If True, all sub-nodes will run to completion even after the failure threshold is met
401410
"""
402411
from flytekit.remote import FlyteLaunchPlan
403412

@@ -407,12 +416,14 @@ def map_task(
407416
concurrency=concurrency,
408417
min_successes=min_successes,
409418
min_success_ratio=min_success_ratio,
419+
run_all_sub_nodes=run_all_sub_nodes,
410420
)
411421
return array_node_map_task(
412422
task_function=target,
413423
concurrency=concurrency,
414424
min_successes=min_successes,
415425
min_success_ratio=min_success_ratio,
426+
run_all_sub_nodes=run_all_sub_nodes,
416427
**kwargs,
417428
)
418429

@@ -422,6 +433,7 @@ def array_node_map_task(
422433
concurrency: Optional[int] = None,
423434
# TODO why no min_successes?
424435
min_success_ratio: float = 1.0,
436+
run_all_sub_nodes: bool = False,
425437
**kwargs,
426438
):
427439
"""Map task that uses the ``ArrayNode`` construct..
@@ -437,8 +449,15 @@ def array_node_map_task(
437449
array node will inherit parallelism from the workflow
438450
:param min_success_ratio: If specified, this determines the minimum fraction of total jobs which can complete
439451
successfully before terminating this task and marking it successful.
452+
:param run_all_sub_nodes: If True, all sub-nodes will run to completion even after the failure threshold is met.
440453
"""
441-
return ArrayNodeMapTask(task_function, concurrency=concurrency, min_success_ratio=min_success_ratio, **kwargs)
454+
return ArrayNodeMapTask(
455+
task_function,
456+
concurrency=concurrency,
457+
min_success_ratio=min_success_ratio,
458+
run_all_sub_nodes=run_all_sub_nodes,
459+
**kwargs,
460+
)
442461

443462

444463
class ArrayNodeMapTaskResolver(tracker.TrackedInstance, TaskResolverMixin):

flytekit/models/core/workflow.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,7 @@ def __init__(
396396
is_original_sub_node_interface=False,
397397
data_mode=None,
398398
bound_inputs=None,
399+
run_all_sub_nodes: bool = False,
399400
) -> None:
400401
"""
401402
TODO: docstring
@@ -409,6 +410,7 @@ def __init__(
409410
self._is_original_sub_node_interface = is_original_sub_node_interface
410411
self._data_mode = data_mode
411412
self._bound_inputs = bound_inputs
413+
self._run_all_sub_nodes = run_all_sub_nodes
412414

413415
@property
414416
def node(self) -> "Node":
@@ -424,6 +426,7 @@ def to_flyte_idl(self) -> _core_workflow.ArrayNode:
424426
is_original_sub_node_interface=BoolValue(value=self._is_original_sub_node_interface),
425427
data_mode=self._data_mode,
426428
bound_inputs=sorted(self._bound_inputs) if self._bound_inputs else None,
429+
run_all_sub_nodes=self._run_all_sub_nodes,
427430
)
428431

429432
@classmethod
@@ -433,6 +436,7 @@ def from_flyte_idl(cls, pb2_object) -> "ArrayNode":
433436
pb2_object.parallelism,
434437
pb2_object.min_successes,
435438
pb2_object.min_success_ratio,
439+
run_all_sub_nodes=pb2_object.run_all_sub_nodes,
436440
)
437441

438442

flytekit/tools/translator.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,7 @@ def get_serializable_array_node(
635635
is_original_sub_node_interface=array_node.is_original_sub_node_interface,
636636
data_mode=array_node.data_mode,
637637
bound_inputs=array_node.bound_inputs,
638+
run_all_sub_nodes=array_node.run_all_sub_nodes,
638639
)
639640

640641

@@ -671,6 +672,7 @@ def get_serializable_array_node_map_task(
671672
execution_mode=entity.execution_mode,
672673
is_original_sub_node_interface=entity.is_original_sub_node_interface,
673674
bound_inputs=entity.bound_inputs,
675+
run_all_sub_nodes=entity.run_all_sub_nodes,
674676
)
675677

676678

tests/flytekit/unit/core/test_array_node.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,3 +378,47 @@ def test_map_task_wrapper():
378378

379379
mapped_lp = map_task(lp)(a=[1, 3, 5], b=[2, 4, 6], c=[7, 8, 9])
380380
assert mapped_lp == [14, 96, 270]
381+
382+
383+
def test_run_all_sub_nodes_default():
384+
node = array_node(lp, concurrency=10, min_success_ratio=0.9)
385+
assert node.run_all_sub_nodes is False
386+
387+
388+
def test_run_all_sub_nodes_set():
389+
node = array_node(lp, concurrency=10, min_success_ratio=0.9, run_all_sub_nodes=True)
390+
assert node.run_all_sub_nodes is True
391+
392+
393+
def test_run_all_sub_nodes_serialization(serialization_settings):
394+
@workflow
395+
def wf_run_all() -> typing.List[int]:
396+
return map_task(lp, concurrency=10, min_success_ratio=0.9, run_all_sub_nodes=True)(
397+
a=[1, 3, 5], b=["two", 4, "six"], c=[7, 8, 9]
398+
)
399+
400+
od = OrderedDict()
401+
wf_spec = get_serializable(od, serialization_settings, wf_run_all)
402+
403+
parent_node = wf_spec.template.nodes[0]
404+
assert parent_node.array_node._run_all_sub_nodes is True
405+
406+
pb = parent_node.array_node.to_flyte_idl()
407+
assert pb.run_all_sub_nodes is True
408+
409+
410+
def test_run_all_sub_nodes_serialization_default(serialization_settings):
411+
@workflow
412+
def wf_no_run_all() -> typing.List[int]:
413+
return map_task(lp, concurrency=10, min_success_ratio=0.9)(
414+
a=[1, 3, 5], b=["two", 4, "six"], c=[7, 8, 9]
415+
)
416+
417+
od = OrderedDict()
418+
wf_spec = get_serializable(od, serialization_settings, wf_no_run_all)
419+
420+
parent_node = wf_spec.template.nodes[0]
421+
assert parent_node.array_node._run_all_sub_nodes is False
422+
423+
pb = parent_node.array_node.to_flyte_idl()
424+
assert pb.run_all_sub_nodes is False

tests/flytekit/unit/core/test_array_node_map_task.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,7 @@ def wf1(x: typing.List[int]):
578578
assert array_node.array_node._parallelism == 10
579579
assert not array_node.array_node._is_original_sub_node_interface
580580
assert array_node.array_node._execution_mode == _core_workflow.ArrayNode.MINIMAL_STATE
581+
assert not array_node.array_node._run_all_sub_nodes
581582
task_spec = od[arraynode_maptask]
582583
assert task_spec.template.metadata.retries.retries == 2
583584
assert task_spec.template.metadata.interruptible
@@ -588,6 +589,67 @@ def wf1(x: typing.List[int]):
588589
assert array_node.array_node._execution_mode == _core_workflow.ArrayNode.FULL_STATE
589590

590591

592+
def test_run_all_sub_nodes_default():
593+
@task
594+
def t1(a: int) -> int:
595+
return a + 1
596+
597+
mt = map_task(t1)
598+
assert mt.run_all_sub_nodes is False
599+
600+
601+
def test_run_all_sub_nodes_set():
602+
@task
603+
def t1(a: int) -> int:
604+
return a + 1
605+
606+
mt = map_task(t1, run_all_sub_nodes=True)
607+
assert mt.run_all_sub_nodes is True
608+
609+
610+
def test_run_all_sub_nodes_serialization(serialization_settings):
611+
@task
612+
def t1(a: int) -> int:
613+
return a + 1
614+
615+
arraynode_maptask = map_task(t1, run_all_sub_nodes=True)
616+
617+
@workflow
618+
def wf(x: typing.List[int]):
619+
return arraynode_maptask(a=x)
620+
621+
od = OrderedDict()
622+
wf_spec = get_serializable(od, serialization_settings, wf)
623+
624+
array_node = wf_spec.template.nodes[0]
625+
assert array_node.array_node._run_all_sub_nodes is True
626+
627+
# Verify it serializes to the protobuf correctly
628+
pb = array_node.array_node.to_flyte_idl()
629+
assert pb.run_all_sub_nodes is True
630+
631+
632+
def test_run_all_sub_nodes_serialization_default(serialization_settings):
633+
@task
634+
def t1(a: int) -> int:
635+
return a + 1
636+
637+
arraynode_maptask = map_task(t1)
638+
639+
@workflow
640+
def wf(x: typing.List[int]):
641+
return arraynode_maptask(a=x)
642+
643+
od = OrderedDict()
644+
wf_spec = get_serializable(od, serialization_settings, wf)
645+
646+
array_node = wf_spec.template.nodes[0]
647+
assert array_node.array_node._run_all_sub_nodes is False
648+
649+
pb = array_node.array_node.to_flyte_idl()
650+
assert pb.run_all_sub_nodes is False
651+
652+
591653
def test_serialization_extended_resources(serialization_settings):
592654
@task(
593655
accelerator=GPUAccelerator("test_gpu"),

0 commit comments

Comments
 (0)