生产级编排AI工作流套件:Flyte全面使用指南 — Programming
Flyte 是一个开源编排器,用于构建生产级数据和机器学习流水线。它以 Kubernetes 作为底层平台,注重可扩展性和可重复性。借助 Flyte,用户团队可以使用 Python SDK 构建流水线,并将其无缝部署在云端和本地环境中,从而实现分布式处理和高效的资源利用。
文中内容仅限技术学习与代码实践参考,市场存在不确定性,技术分析需谨慎验证,不构成任何投资建议。
链式调用实体
Flyte 提供了使用 >>
运算符进行实体链式调用的机制。这在需要将任务(task)和子工作流(subworkflow)串联执行且无需实体间数据流传递的场景中特别有用。
任务
以下示例展示了 t1()
在 t0()
之后执行,t2()
在 t1()
之后执行的链式调用关系:
import flytekit as fl
@fl.task
def t2():
print("Running t2")
return
@fl.task
def t1():
print("Running t1")
return
@fl.task
def t0():
print("Running t0")
return
# 链式调用任务
@fl.workflow
def chain_tasks_wf():
t2_promise = t2()
t1_promise = t1()
t0_promise = t0()
t0_promise >> t1_promise
t1_promise >> t2_promise
子工作流
与任务类似,子工作流也可以进行链式调用:
@fl.workflow
def sub_workflow_1():
t1()
@fl.workflow
def sub_workflow_0():
t0()
@fl.workflow
def chain_workflows_wf():
sub_wf1 = sub_workflow_1()
sub_wf0 = sub_workflow_0()
sub_wf0 >> sub_wf1
注意:在本地 Python 环境中不支持对任务和子工作流进行链式调用。
条件语句
Flytekit 将条件语句提升为一等公民构造 conditional
,提供了在 workflow 中选择性执行分支的强大机制。条件语句可基于任务生成的静态/动态数据或 workflow 输入数据进行评估。虽然条件评估具有高性能特性,但需要注意它们仅限于特定的二元和逻辑运算符,且仅适用于原始值。
首先导入必要的库:
import random
import flytekit as fl
from flytekit import conditional
from flytekit.core.task import Echo
简单分支
本例演示两个任务 calculate_circle_circumference
和 calculate_circle_area
。workflow 根据输入值是否在分数范围 (0-1) 内动态选择执行哪个任务:
@fl.task
def calculate_circle_circumference(radius: float) -> float:
return 2 * 3.14 * radius # 计算圆周长的任务
@fl.task
def calculate_circle_area(radius: float) -> float:
return 3.14 * radius * radius # 计算圆面积的任务
@fl.workflow
def shape_properties(radius: float) -> float:
return (
conditional("shape_properties")
.if_((radius >= 0.1) & (radius < 1.0))
.then(calculate_circle_circumference(radius=radius))
.else_()
.then(calculate_circle_area(radius=radius))
)
if __name__ == "__main__":
radius_small = 0.5
print(f"小圆周长(半径={radius_small}): {shape_properties(radius=radius_small)}")
radius_large = 3.0
print(f"大圆面积(半径={radius_large}): {shape_properties(radius=radius_large)}")
多分支
建立包含多个分支的 if
条件语句,若所有条件均不满足将返回失败。注意 Flyte 中的任何 conditional
语句都必须是完整的,即必须覆盖所有可能分支:
@fl.workflow
def shape_properties_with_multiple_branches(radius: float) -> float:
return (
conditional("shape_properties_with_multiple_branches")
.if_((radius >= 0.1) & (radius < 1.0))
.then(calculate_circle_circumference(radius=radius))
.elif_((radius >= 1.0) & (radius <= 10.0))
.then(calculate_circle_area(radius=radius))
.else_()
.fail("输入必须介于 0 到 10 之间")
)
注意位运算符 (&
) 的使用。由于 Python PEP-335 的限制,逻辑运算符 and
、or
和 not
无法被重载。Flytekit 使用位运算符 &
和 |
分别作为逻辑 and
和 or
的等效形式,这也与其他库的惯例一致。
使用条件语句的输出
编写一个消费 conditional
返回输出的任务:
@fl.workflow
def shape_properties_accept_conditional_output(radius: float) -> float:
result = (
conditional("shape_properties_accept_conditional_output")
.if_((radius >= 0.1) & (radius < 1.0))
.then(calculate_circle_circumference(radius=radius))
.elif_((radius >= 1.0) & (radius <= 10.0))
.then(calculate_circle_area(radius=radius))
.else_()
.fail("输入必须介于 0 到 10 之间")
)
return calculate_circle_area(radius=result)
if __name__ == "__main__":
radius_small = 0.5
print(
f"小圆周长(半径={radius_small})x 圆面积(半径={calculate_circle_circumference(radius=radius_small)}): {shape_properties_accept_conditional_output(radius=radius_small)}"
)
在条件语句中使用前序任务的输出
可以检查前序任务返回的布尔值是否为 True
,但不直接支持一元操作。需使用结果的 is_true
、is_false
和 is_none
方法:
@fl.task
def coin_toss(seed: int) -> bool:
"""模拟操作执行成功的条件检查"""
r = random.Random(seed)
if r.random() < 0.5:
return True
return False
@fl.task
def failed() -> int:
"""模拟失败处理任务"""
return -1
@fl.task
def success() -> int:
"""模拟成功处理任务"""
return 0
@fl.workflow
def boolean_wf(seed: int = 5) -> int:
result = coin_toss(seed=seed)
return conditional("coin_toss").if_(result.is_true()).then(success()).else_().then(failed())
[!NOTE]
输出值如何获得这些方法? 在 workflow 中不能直接访问输出,输入和输出会被自动封装到名为
flytekit.extend.Promise
的特殊对象中。
在条件语句中使用布尔型 workflow 输入
可以直接传递布尔值到 workflow:
@fl.workflow
def boolean_input_wf(boolean_input: bool) -> int:
return conditional("boolean_input_conditional").if_(boolean_input.is_true()).then(success()).else_().then(failed())
注意传递的布尔值拥有 is_true
方法。这个布尔值存在于 workflow 上下文中,并被封装在 Flytekit 的特殊对象中,使其具有扩展行为。
本地运行 workflow 示例:
if __name__ == "__main__":
print("多次运行 boolean_wf...")
for index in range(0, 5):
print(f"boolean_wf 生成输出 = {boolean_wf(seed=index)}")
print(
f"布尔输入:{True if index < 2 else False}; workflow 输出:{boolean_input_wf(boolean_input=True if index < 2 else False)}"
)
嵌套条件语句
可以在其他条件语句的 then
部分任意嵌套条件块:
@fl.workflow
def nested_conditions(radius: float) -> float:
return (
conditional("nested_conditions")
.if_((radius >= 0.1) & (radius < 1.0))
.then(
conditional("inner_nested_conditions")
.if_(radius < 0.5)
.then(calculate_circle_circumference(radius=radius))
.elif_((radius >= 0.5) & (radius < 0.9))
.then(calculate_circle_area(radius=radius))
.else_()
.fail("0.9 是异常值")
)
.elif_((radius >= 1.0) & (radius <= 10.0))
.then(calculate_circle_area(radius=radius))
.else_()
.fail("输入必须介于 0 到 10 之间")
)
if __name__ == "__main__":
print(f"nested_conditions(0.4): {nested_conditions(radius=0.4)}")
在条件语句中使用任务的输出
编写一个趣味 workflow:当抛硬币结果为"正面"时触发 calculate_circle_circumference
任务,为"反面"时运行 calculate_circle_area
任务:
@fl.workflow
def consume_task_output(radius: float, seed: int = 5) -> float:
is_heads = coin_toss(seed=seed)
return (
conditional("double_or_square")
.if_(is_heads.is_true())
.then(calculate_circle_circumference(radius=radius))
.else_()
.then(calculate_circle_area(radius=radius))
)
if __name__ == "__main__":
default_seed_output = consume_task_output(radius=0.4)
print(
f"执行 consume_task_output(0.4),默认 seed=5。预期输出:calculate_circle_area => {default_seed_output}"
)
custom_seed_output = consume_task_output(radius=0.4, seed=7)
print(
f"执行 consume_task_output(0.4, seed=7)。预期输出:calculate_circle_circumference => {custom_seed_output}"
)
在条件语句中执行空操作任务
当特定条件不满足时,可能需要跳过条件 workflow 的执行。可以使用 echo
任务直接返回输入值:
在 Flyte 配置文件中启用 echo 插件:
task-plugins:
enabled-plugins:
- echo
echo = Echo(name="echo", inputs={"radius": float})
@fl.workflow
def noop_in_conditional(radius: float, seed: int = 5) -> float:
is_heads = coin_toss(seed=seed)
return (
conditional("noop_in_conditional")
.if_(is_heads.is_true())
.then(calculate_circle_circumference(radius=radius))
.else_()
.then(echo(radius=radius))
)
在 Flyte 集群上运行示例
使用以下命令在 Flyte 集群上运行 workflow:
$ pyflyte run --remote \
https://raw.githubusercontent.com/flyteorg/flytesnacks/656e63d1c8dded3e9e7161c7af6425e9fcd43f56/examples/advanced_composition/advanced_composition/conditional.py \
shape_properties --radius 3.0
$ pyflyte run --remote \
https://raw.githubusercontent.com/flyteorg/flytesnacks/656e63d1c8dded3e9e7161c7af6425e9fcd43f56/examples/advanced_composition/advanced_composition/conditional.py \
shape_properties_with_multiple_branches --radius 11.0
$ pyflyte run --remote \
https://raw.githubusercontent.com/flyteorg/flytesnacks/656e63d1c8dded3e9e7161c7af6425e9fcd43f56/examples/advanced_composition/advanced_composition/conditional.py \
shape_properties_accept_conditional_output --radius 0.5
$ pyflyte run --remote \
https://raw.githubusercontent.com/flyteorg/flytesnacks/656e63d1c8dded3e9e7161c7af6425e9fcd43f56/examples/advanced_composition/advanced_composition/conditional.py \
boolean_wf
$ pyflyte run --remote \
https://raw.githubusercontent.com/flyteorg/flytesnacks/656e63d1c8dded3e9e7161c7af6425e9fcd43f56/examples/advanced_composition/advanced_composition/conditional.py \
boolean_input_wf --boolean_input
$ pyflyte run --remote \
https://raw.githubusercontent.com/flyteorg/flytesnacks/656e63d1c8dded3e9e7161c7af6425e9fcd43f56/examples/advanced_composition/advanced_composition/conditional.py \
nested_conditions --radius 0.7
$ pyflyte run --remote \
https://raw.githubusercontent.com/flyteorg/flytesnacks/656e63d1c8dded3e9e7161c7af6425e9fcd43f56/examples/advanced_composition/advanced_composition/conditional.py \
consume_task_output --radius 0.4 --seed 7
$ pyflyte run --remote \
https://raw.githubusercontent.com/flyteorg/flytesnacks/656e63d1c8dded3e9e7161c7af6425e9fcd43f56/examples/advanced_composition/advanced_composition/conditional.py \
noop_in_conditional --radius 0.4 --seed 5
装饰任务
您可以通过使用装饰器包装任务函数,轻松改变任务的行为方式。
为了确保被装饰的函数包含Flyte所需的所有类型注解和文档字符串信息,需要使用内置的functools.wraps
装饰器。
创建文件
首先创建名为decorating_tasks.py
的文件。
添加导入语句
import logging
import flytekit as fl
from functools import partial, wraps
创建日志记录器
添加日志记录器以监控执行进度:
logger = logging.getLogger(__file__)
使用单个装饰器
定义记录任务输入输出详情的装饰器:
def log_io(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
logger.info(f"任务 {fn.__name__} 被调用,参数 args: {args}, kwargs: {kwargs}")
out = fn(*args, **kwargs)
logger.info(f"任务 {fn.__name__} 输出结果: {out}")
return out
return wrapper
创建使用log_io
装饰的任务t1
。
重要:装饰器调用顺序需注意,@task
必须始终作为最外层装饰器。
@fl.task
@log_io
def t1(x: int) -> int:
return x + 1
堆叠多个装饰器
只要保持@task
为最外层装饰器,可以堆叠多个装饰器。
定义验证输出值的装饰器,确保返回值为正数:
def validate_output(fn=None, *, floor=0):
@wraps(fn)
def wrapper(*args, **kwargs):
out = fn(*args, **kwargs)
if out <= floor:
raise ValueError(f"任务 {fn.__name__} 的输出必须是正数,当前值 {out}")
return out
if fn is None:
return partial(validate_output, floor=floor)
return wrapper
validate_output
使用functools.partial
实现参数化装饰器。
定义同时使用日志记录和验证装饰器的函数:
@fl.task
@log_io
@validate_output(floor=10)
def t2(x: int) -> int:
return x + 10
组合工作流
创建调用t1
和t2
的工作流:
@fl.workflow
def decorating_task_wf(x: int) -> int:
return t2(x=t1(x=x))
在Flyte上运行示例
执行以下命令运行工作流:
pyflyte run --remote \
https://raw.githubusercontent.com/flyteorg/flytesnacks/69dbe4840031a85d79d9ded25f80397c6834752d/examples/advanced_composition/advanced_composition/decorating_tasks.py \
decorating_task_wf --x 10
装饰工作流
通过使用内置的functools.wraps
装饰器模式,可以以轻量级方式修改工作流的行为,类似于使用装饰器自定义任务行为。但与任务装饰不同,我们需要进行额外操作来确保工作流底层DAG以正确顺序执行任务。
初始化-清理模式
装饰@fl.workflow
函数的主要用例是建立初始化-清理模式,在主工作流逻辑前后执行任务。这在集成外部服务(如wandb或clearml)时非常有用,这些服务可用于跟踪模型训练运行的指标。
首先创建名为decorating_workflows
的文件:
导入必要库:
from functools import partial, wraps
from unittest.mock import MagicMock
import flytekit as fl
from flytekit import FlyteContextManager
from flytekit.core.node_creation import create_node
定义初始化与清理任务。本示例使用unittest.mock.MagicMock
类创建模拟外部服务,需要在工作流开始初始化和结束时完成:
external_service = MagicMock()
@fl.task
def setup():
print("initializing external service")
external_service.initialize(id=flytekit.current_context().execution_id)
@fl.task
def teardown():
print("finish external service")
external_service.complete(id=flytekit.current_context().execution_id)
通过Flytekit的当前上下文可获取工作流的execution_id
,用于将Flyte与外部服务关联,确保两者使用相同唯一标识符。
工作流装饰器
创建用于包装工作流函数的装饰器:
def setup_teardown(fn=None, *, before, after):
@wraps(fn)
def wrapper(*args, **kwargs):
# 获取当前Flyte上下文以访问工作流DAG的编译状态
ctx = FlyteContextManager.current_context()
# 定义前置节点
before_node = create_node(before)
# ctx.compilation_state.nodes == [before_node]
# 底层Flytekit编译器定义并连接`my_workflow`函数体内的节点
outputs = fn(*args, **kwargs)
# ctx.compilation_state.nodes == [before_node, *nodes_created_by_fn]
# 定义后置节点
after_node = create_node(after)
# ctx.compilation_state.nodes == [before_node, *nodes_created_by_fn, after_node]
# 通过确保`before_node`在主工作流第一个节点前执行,
# `after_node`在最后一个节点后执行来正确编译工作流
if ctx.compilation_state is not None:
# ctx.compilation_state.nodes是按上述执行顺序定义的节点列表
workflow_node0 = ctx.compilation_state.nodes[1]
workflow_node1 = ctx.compilation_state.nodes[-2]
before_node >> workflow_node0
workflow_node1 >> after_node
return outputs
if fn is None:
return partial(setup_teardown, before=before, after=after)
return wrapper
上述setup_teardown
装饰器的关键点:
- 接收
before
和after
参数,二者必须为@fl.task
装饰的任务函数,分别在主工作流前后执行 - 使用create_node函数创建与前后任务关联的节点
- 调用
fn
时,系统会自动创建与工作流函数体关联的所有节点 if ctx.compilation_state is not None:
条件内的代码在编译时执行,通过索引1
和-2
获取主工作流函数体的首尾节点>>
右移运算符确保before_node
在主工作流首个节点前执行,after_node
在最后一个节点后执行
定义DAG
定义构成工作流的两个任务:
@fl.task
def t1(x: float) -> float:
return x - 1
@fl.task
def t2(x: float) -> float:
return x**2
创建装饰后的工作流:
@fl.workflow
@setup_teardown(before=setup, after=teardown)
def decorating_workflow(x: float) -> float:
return t2(x=t1(x=x))
在Flyte集群运行示例
使用以下命令在Flyte集群运行工作流:
pyflyte run --remote \
https://raw.githubusercontent.com/flyteorg/flytesnacks/69dbe4840031a85d79d9ded25f80397c6834752d/examples/advanced_composition/advanced_composition/decorating_workflows.py \
decorating_workflow --x 10.0
任务内检查点
Flyte 中的检查点机制通过保存任务失败前的状态,使得任务可以从最近记录的状态恢复执行,从而实现故障恢复。
为何需要任务内检查点?
Flyte 作为工作流引擎的固有设计,允许用户将操作、程序或概念分解为工作流中的多个小型任务。当任务失败时,工作流无需重新运行已完成的先前任务,只需重试出现问题的特定任务。一旦问题任务成功执行,便不会再次运行。因此,任务之间的自然边界形成了隐式检查点。
然而在某些场景中,将任务拆分为更小的任务可能面临挑战或产生额外开销,这在紧密循环中执行大量计算时尤为明显。虽然用户可以使用动态工作流将每次循环迭代拆分为独立任务,但创建新任务、记录中间结果和重建状态的开销会带来额外成本。
典型用例:模型训练
模型训练是展示任务内检查点实用性的典型案例场景。在使用相同数据集执行多轮次(epoch)或多迭代时,设置任务边界可能导致高昂的引导时间成本。
Flyte 通过提供任务执行内部的检查点机制来解决这一挑战,将进度保存为文件或文件集。当发生故障时,可以通过重新读取检查点文件恢复大部分状态,避免重新运行整个任务。此特性使得使用更经济的计算系统(如 AWS spot 实例、GCP 抢占式实例)成为可能。
相较于按需或预留实例,这些实例能以显著更低的价格提供优异性能。当任务以容错方式构建时,这种经济性优势得以实现。对于短时任务(例如 10 分钟内),失败概率可忽略不计,基于任务边界的恢复机制能提供足够的容错保障。
但随着任务执行时间的增长,重新运行的成本也随之增加,成功完成的概率降低。这正是 Flyte 任务内检查点展现巨大价值的场景。
以下示例展示如何开发利用任务内检查点的任务。需注意 Flyte 当前提供底层检查点 API,未来计划集成 Keras、PyTorch、Scikit-learn 等训练框架及 Spark、Flink 等大数据框架的高阶检查点 API,增强其容错能力。
创建 checkpoint.py
文件:
导入所需库:
import flytekit as fl
from flytekit.exceptions.user import FlyteRecoverableException
RETRIES = 3
定义精确迭代 n_iterations
的任务,保存检查点并从模拟故障中恢复:
# 定义精确迭代`n_iterations`的任务,保存检查点并从模拟故障中恢复
@fl.task(retries=RETRIES)
def use_checkpoint(n_iterations: int) -> int:
cp = fl.current_context().checkpoint
prev = cp.read()
start = 0
if prev:
start = int(prev.decode())
# 创建故障间隔来模拟跨'n'次迭代的故障,在配置重试后成功
failure_interval = n_iterations // RETRIES
index = 0
for index in range(start, n_iterations):
# 模拟确定性故障以进行演示。展示如何在配置的重试次数内最终完成
if index > start and index % failure_interval == 0:
raise FlyteRecoverableException(f"Failed at iteration {index}, failure_interval {failure_interval}.")
# 保存进度状态。也可以选择每隔若干间隔保存状态
cp.write(f"{index + 1}".encode())
return index
检查点系统提供额外 API,代码详见 checkpointer 实现。
创建调用任务的工作流:当发生 FlyteRecoverableException 时任务会自动重试:
@fl.workflow
def checkpointing_example(n_iterations: int) -> int:
return use_checkpoint(n_iterations=n_iterations)
本地运行时未使用检查点(因不支持重试):
if __name__ == "__main__":
try:
checkpointing_example(n_iterations=10)
except RuntimeError as e: # noqa : F841
# 本地运行时期望出现的异常(未执行重试)
pass
在 Flyte 集群运行示例
使用以下命令在 Flyte 集群运行工作流:
pyflyte run --remote \
https://raw.githubusercontent.com/flyteorg/flytesnacks/69dbe4840031a85d79d9ded25f80397c6834752d/examples/advanced_composition/advanced_composition/checkpoint.py \
checkpointing_example --n_iterations 10
pyflyte run --remote \
https://raw.githubusercontent.com/flyteorg/flytesnacks/69dbe4840031a85d79d9ded25f80397c6834752d/examples/advanced_composition/advanced_composition/checkpoint.py \
checkpointing_example --n_iterations 10
等待外部输入
在某些用例中,您可能希望工作流执行暂停,直到经过特定时间或接收到工作流执行输入之外的外部输入后再继续。可以将这些视为执行时输入,因为它们需要在工作流启动后提供。此类用例的示例包括:
- 模型部署:需要训练
n
个模型的超参数调优工作流,在将模型部署到服务层之前需要人工检查报告并批准。 - 数据标注:遍历图像数据集的工作流,将单个图像呈现给人工标注员进行标注。
- 主动学习:主动学习工作流,训练模型后根据模型最不确定/最确定或能提供最多信息的样本,展示给人工标注员进行标注。
在Flyte中可以通过flytekit.sleep
、flytekit.wait_for_input
和flytekit.approve
工作流节点实现这些用例。尽管上述示例都是需要人工参与的流程,但这些构造允许您从任意外部流程(人工或机器)向工作流传递输入以继续执行。
这些函数只能在@fl.workflow
修饰的函数、@fl.dynamic
修饰的函数或命令式工作流中使用。
使用sleep
节点暂停执行
最简单的情况是让工作流在执行前flytekit.sleep
指定的时间。
尽管这种节点在生产环境中不常用,但您可以用它来模拟工作流中的延迟,例如模拟长时间运行的计算行为。
from datetime import timedelta
import flytekit as fl
from flytekit import sleep
@fl.task
def long_running_computation(num: int) -> int:
"""模拟长时间运行计算的mock任务"""
return num
@fl.workflow
def sleep_wf(num: int) -> int:
"""使用sleep模拟'长时间运行'的计算"""
# 增加sleep时间使计算真正长时间运行
sleeping = sleep(timedelta(seconds=10))
result = long_running_computation(num=num)
sleeping >> result
return result
如上所示,我们定义了一个简单的add_one
任务和sleep_wf
工作流。首先创建sleeping
和result
节点,然后使用>>
运算符排序依赖关系,使工作流在启动result
计算前休眠10秒。最后返回result
。
可通过此处了解更多关于>>
链式运算符的信息。
现在您已了解基本工作原理,接下来让我们看看flytekit.wait_for_input
工作流节点。
使用wait_for_input
提供外部输入
通过flytekit.wait_for_input
节点,可以暂停需要外部输入信号的工作流执行。例如,假设您有一个发布自动化分析报告的工作流,但希望在发布前为其添加自定义标题:
import typing
from flytekit import wait_for_input
@fl.task
def create_report(data: typing.List[float]) -> dict: # o0
"""示例报告任务"""
return {
"mean": sum(data) / len(data),
"length": len(data),
"max": max(data),
"min": min(data),
}
@fl.task
def finalize_report(report: dict, title: str) -> dict:
return {"title": title, **report}
@fl.workflow
def reporting_wf(data: typing.List[float]) -> dict:
report = create_report(data=data)
title_input = wait_for_input("title", timeout=timedelta(hours=1), expected_type=str)
return finalize_report(report=report, title=title_input)
让我们解析上述代码:
- 在
reporting_wf
中首先创建原始report
- 然后定义
title
节点,该节点将通过Flyte API(可通过Flyte UI或FlyteRemote
实现)等待字符串输入,1小时后超时 - 最后将
title_input
传递给finalize_report
,将自定义标题附加到报告
create_report
任务只是简单示例。实际场景中,报告可能是HTML文件或可视化图表集,可通过Flyte Decks在Flyte UI中渲染。
如本文开头所述,该构造可用于在缺乏明确单一指标确定最佳模型时选择最佳模型,或在使用Flyte工作流进行数据标注的场景。
使用approve
继续执行
最后,flytekit.approve
工作流节点允许在收到明确批准信号后继续执行。回到报告发布的用例,假设我们希望在某些情况下阻止报告发布(例如报告有效性存疑):
from flytekit import approve
@fl.workflow
def reporting_with_approval_wf(data: typing.List[float]) -> dict:
report = create_report(data=data)
title_input = wait_for_input("title", timeout=timedelta(hours=1), expected_type=str)
final_report = finalize_report(report=report, title=title_input)
# 批准最终报告,approve节点的输出是final_report字典
return approve(final_report, "approve-final-report", timeout=timedelta(hours=2))
approve
节点将通过工作流输出传递final_report
承诺,前提是approve-final-report
通过Flyte UI或Flyte API获得批准输入。
您还可以将approve
函数的输出作为承诺传递给后续任务。创建报告发布工作流的另一个版本,在create_report
之后进行审批:
@fl.workflow
def approval_as_promise_wf(data: typing.List[float]) -> dict:
report = create_report(data=data)
title_input = wait_for_input("title", timeout=timedelta(hours=1), expected_type=str)
# 等待报告运行,以便用户在添加自定义标题前查看
report >> title_input
final_report = finalize_report(
report=approve(report, "raw-report-approval", timeout=timedelta(hours=2)),
title=title_input,
)
return final_report
条件语句的使用
这些节点构造本身很有用,但与条件语句等Flyte其他构造结合使用时效果更佳。
扩展报告发布用例,在未批准最终报告时生成"invalid report"输出:
from flytekit import conditional
@fl.task
def invalid_report() -> dict:
return {"invalid_report": True}
@fl.workflow
def conditional_wf(data: typing.List[float]) -> dict:
report = create_report(data=data)
title_input = wait_for_input("title-input", timeout=timedelta(hours=1), expected_type=str)
# 定义"review-passes" wait_for_input节点,供人工在最终确定前审查报告
review_passed = wait_for_input("review-passes", timeout=timedelta(hours=2), expected_type=bool)
report >> review_passed
# 该条件语句在审查通过时返回最终报告,否则返回无效报告输出
return (
conditional("final-report-condition")
.if_(review_passed.is_true())
.then(finalize_report(report=report, title=title_input))
.else_()
.then(invalid_report())
)
除了在conditional
中用于确定执行分支的approved
节点外,我们还定义了disapprove_reason
门控节点,作为invalid_report
任务的输入。
向wait_for_input
和approve
节点发送输入
假设您已在通过flytectl demo start启动的Flyte集群上注册了上述工作流,有两种使用wait_for_input
和approve
节点的方法:
使用Flyte UI
在Flyte UI上启动reporting_wf
工作流时,会看到如下执行图视图:
点击title
任务节点的播放图标或侧边栏的Resume按钮,将弹出表单用于提供自定义标题输入。
使用FlyteRemote
对于多数情况,使用Flyte UI即可满足门控节点的输入/批准需求。但若需要以编程方式传递输入,可使用FlyteRemote.set_signal
方法。以下示例展示如何为gate_node_with_conditional_wf
工作流设置title-input
和review-passes
节点的值:
import typing
from flytekit.remote.remote import FlyteRemote
from flytekit.configuration import Config
remote = FlyteRemote(
Config.for_sandbox(),
default_project="flytesnacks",
default_domain="development",
)
# 首先启动工作流
flyte_workflow = remote.fetch_workflow(
name="core.control_flow.waiting_for_external_inputs.conditional_wf"
)
# 执行工作流
execution = remote.execute(flyte_workflow, inputs={"data": [1.0, 2.0, 3.0, 4.0, 5.0]})
# 获取执行可用的信号列表
signals = remote.list_signals(execution.id.name)
# 为"title"节点设置信号值。确保"title-input"节点在signals列表中
remote.set_signal("title-input", execution.id.name, "my report")
# 为"review-passes"节点设置信号值。确保"review-passes"节点在signals列表中
remote.set_signal("review-passes", execution.id.name, True)
嵌套并行性
对于无法通过动态工作流或map任务充分实现的超大规模或复杂工作流,采用多层工作流并行化会带来显著优势。
这种方法的优势包括:
- 更好的代码组织性
- 更好的代码复用性
- 更便捷的测试
- 更高效的调试
- 更细粒度的监控(每个子工作流可独立运行和监控)
- 更高的性能和扩展性(每个子工作流作为独立单元执行,可分布在不同的propeller worker和分片上,实现更好的并行处理能力)
嵌套动态工作流
通过嵌套动态工作流可将大型工作流拆分为多个小型工作流,形成层级结构。以下示例展示了一个顶层工作流使用两级动态工作流处理整数列表,并通过简单加法任务最终将列表扁平化。
示例代码
"""
核心工作流以6个元素为处理单位,分块大小为2时的结构如下:
multi_wf -> level1 -> level2 -> core_wf -> step1 -> step2
-> core_wf -> step1 -> step2
level2 -> core_wf -> step1 -> step2
-> core_wf -> step1 -> step2
level2 -> core_wf -> step1 -> step2
-> core_wf -> step1 -> step2
"""
import flytekit as fl
@fl.task
def step1(a: int) -> int:
return a + 1
@fl.task
def step2(a: int) -> int:
return a + 2
@fl.workflow
def core_wf(a: int) -> int:
return step2(a=step1(a=a))
core_wf_lp = fl.LaunchPlan.get_or_create(core_wf)
@fl.dynamic
def level2(l: list[int]) -> list[int]:
return [core_wf_lp(a=a) for a in l]
@fl.task
def reduce(l: list[list[int]]) -> list[int]:
f = []
for i in l:
f.extend(i)
return f
@fl.dynamic
def level1(l: list[int], chunk: int) -> list[int]:
v = []
for i in range(0, len(l), chunk):
v.append(level2(l=l[i:i + chunk]))
return reduce(l=v)
@fl.workflow
def multi_wf(l: list[int], chunk: int) -> list[int]:
return level1(l=l, chunk=chunk)
通过覆盖(overrides)可以为动态循环中的启动计划添加额外参数。以下示例添加缓存配置:
@fl.task
def increment(num: int) -> int:
return num + 1
@fl.workflow
def child(num: int) -> int:
return increment(num=num)
child_lp = fl.LaunchPlan.get_or_create(child)
@fl.dynamic
def spawn(n: int) -> list[int]:
l = []
for i in [1,2,3,4,5]:
l.append(child_lp(num=i).with_overrides(cache=True, cache_version="1.0.0"))
# 可将列表传递给其他任务
return l
混合并行性
此示例与嵌套动态工作流类似,但核心工作流通过map任务实现并行处理而非串行任务。虽然减少了一层并行结构导致输出不同,但仍展示了如何混合不同方法实现并发。
示例代码
"""
核心工作流以6个元素为处理单位,分块大小为2时的结构如下:
multi_wf -> level1 -> level2 -> mappable
-> mappable
level2 -> mappable
-> mappable
level2 -> mappable
-> mappable
"""
import flytekit as fl
@fl.task
def mappable(a: int) -> int:
return a + 2
@fl.workflow
def level2(l: list[int]) -> list[int]:
return fl.map_task(mappable)(a=l)
@fl.task
def reduce(l: list[list[int]]) -> list[int]:
f = []
for i in l:
f.extend(i)
return f
@fl.dynamic
def level1(l: list[int], chunk: int) -> list[int]:
v = []
for i in range(0, len(l), chunk):
v.append(level2(l=l[i : i + chunk]))
return reduce(l=v)
@fl.workflow
def multi_wf(l: list[int], chunk: int) -> list[int]:
return level1(l=l, chunk=chunk)
设计考量
虽然可以实现更深层次的嵌套或在输入类型统一时使用map任务,但工作流设计应基于实际数据处理需求。例如处理音乐库歌词提取时:
- 第一级循环处理专辑
- 第二级处理单曲
对于处理同类型输入的大规模列表,建议保持代码简洁,由调度器优化执行。除非需要动态工作流的输入输出混合特性,否则优先使用map任务(兼具界面简洁优势)。
可通过以下方式限制并行规模:
- 工作流层级的max_parallelism属性(默认25)限制并行任务数
- map任务中通过concurrency参数控制并行执行数
失败节点
失败节点功能允许您指定一个特定节点,以便在工作流内部发生故障时执行。
例如,某个工作流首先创建集群,随后执行任务,最终在所有任务完成后删除集群。然而,如果工作流中的任何任务发生错误,系统将中止整个工作流且不会删除集群。这在任务失败后仍需清理集群时会产生问题。
为解决此问题,您可以在工作流中添加失败节点。这能确保关键操作(如删除集群)即使在整体工作流执行过程中发生故障时仍能执行。
import typing
import flytekit as fl
from flytekit import WorkflowFailurePolicy
from flytekit.types.error.error import FlyteError
@fl.task
def create_cluster(name: str):
print(f"Creating cluster: {name}")
创建将在执行过程中失败的任务:
# 创建一个将在执行过程中失败的任务
@fl.task
def t1(a: int, b: str):
print(f"{a} {b}")
raise ValueError("Fail!")
创建在工作流中任何任务失败时执行的任务:
@fl.task
def clean_up(name: str, err: typing.Optional[FlyteError] = None):
print(f"Deleting cluster {name} due to {err}")
将 on_failure
指定为清理任务。该任务将在工作流中任何任务失败时执行。
clean_up
的输入必须与工作流的输入完全匹配。此外,err
参数将被填充为执行期间遇到的错误信息。
@fl.workflow
def wf(a: int, b: str):
create_cluster(name=f"cluster-{a}")
t1(a=a, b=b)
通过将失败策略设置为 FAIL_AFTER_EXECUTABLE_NODES_COMPLETE
来确保即使子工作流失败也执行 wf1
。在此场景中,父工作流和子工作流都将失败,导致 clean_up
任务被执行两次:
# 在此场景中,父工作流和子工作流都将失败,
# 导致 `clean_up` 任务被执行两次。
@fl.workflow(on_failure=clean_up, failure_policy=WorkflowFailurePolicy.FAIL_AFTER_EXECUTABLE_NODES_COMPLETE)
def wf1(name: str = "my_cluster"):
c = create_cluster(name=name)
subwf(name="another_cluster")
t = t1(a=1, b="2")
d = delete_cluster(name=name)
c >> t >> d
您也可以将 on_failure
设置为工作流。该工作流将在任何任务失败时执行:
@fl.workflow(on_failure=clean_up_wf)
def wf2(name: str = "my_cluster"):
c = create_cluster(name=name)
t = t1(a=1, b="2")
d = delete_cluster(name=name)
c >> t >> d
风险提示与免责声明
本文内容基于公开信息研究整理,不构成任何形式的投资建议。历史表现不应作为未来收益保证,市场存在不可预见的波动风险。投资者需结合自身财务状况及风险承受能力独立决策,并自行承担交易结果。作者及发布方不对任何依据本文操作导致的损失承担法律责任。市场有风险,投资须谨慎。