生产级编排AI工作流套件:Flyte全面使用指南 — Core concepts Tasks

生产级编排AI工作流套件:Flyte全面使用指南 — Core concepts Tasks

Flyte 是一个开源编排器,用于构建生产级数据和机器学习流水线。它以 Kubernetes 作为底层平台,注重可扩展性和可重复性。借助 Flyte,用户团队可以使用 Python SDK 构建流水线,并将其无缝部署在云端和本地环境中,从而实现分布式处理和高效的资源利用。

文中内容仅限技术学习与代码实践参考,市场存在不确定性,技术分析需谨慎验证,不构成任何投资建议。

Flyte

任务

任务是 Flyte 中计算的基本单元。它们是可独立执行、强类型且容器化的构建块,用于组成工作流。工作流通过将任务链式连接构建而成,一个任务的输出作为下一个任务的输入,形成有向无环图。

任务可独立执行

任务设计为可独立执行,意味着它们可以与其他任务隔离运行。由于大多数任务只是 Python 函数,它们可以在本地机器上执行,这使得在部署到 Flyte 之前能够方便地进行本地单元测试和调试。

由于任务可独立执行,它们也可以跨多个工作流共享和复用。只要任务逻辑具有确定性,其输入和输出就可以缓存以节省计算资源和执行时间。

任务具有强类型

任务具有强类型的输入和输出,这些类型会在部署时进行验证。这有助于早期发现错误,并确保通过任务和工作流传递的数据与显式声明的类型兼容。

底层实现中,Flyte 使用Flyte 类型系统,并在 Flyte 类型与 Python 类型之间进行转换。Python 类型注解确保通过任务和工作流传递的数据与函数签名中定义的显式声明类型兼容。Flyte 类型系统还用于缓存、数据溯源跟踪,以及在任务间传递数据时的自动序列化和反序列化。

任务容器化

虽然(大多数)任务可在本地执行,但当任务作为注册流程的一部分部署到 Flyte 时,它会被容器化并在独立的 Kubernetes pod 中运行。

这使得任务可以拥有独立的软件依赖项硬件要求。例如:

  • 需要 GPU 的任务可以使用支持 GPU 的容器镜像部署
  • 需要特定版本软件库的任务可以安装该版本库进行部署

任务具有名称、版本且不可变

任务的完全限定名由其项目(project)、域(domain)和名称(name)组成。要更新任务,您需要修改它并在相同的完全限定名下重新注册。这会创建新版本的任务,同时旧版本仍然可用。因此在版本层面,任务是不可变的。这种不可变性对于确保工作流的可重复性和数据溯源的准确性至关重要。

任务(通常)具有确定性和可缓存性

在确定某个执行单元是否适合封装为任务时,请考虑以下问题:

  • 任务是否有明确定义的优雅/成功退出条件?
    • 任务应在完成输入处理后退出
  • 是否具有确定性和可重复性?
    • 在某些情况下,任务可能会被缓存或用相同输入重新运行。每次都应产生相同输出。例如应避免使用以当前时钟为种子的随机数生成器
  • 是否是纯函数?即是否存在系统未知的副作用?
    • 建议避免在任务中使用副作用
    • 当副作用不可避免时,应确保操作具有幂等性

有关任务缓存的详细信息,请参阅缓存

工作流可包含多种任务类型

Flyte 最强大的功能之一是可以将完全不同的计算工作负载作为任务在单个工作流中运行。

由于 Flyte 的架构设计,单个工作流中的任务可以在多个维度上存在差异。虽然任务可配置的方式很多,但选项主要分为三类:

  • 任务类型:包括标准 Python 任务、map 任务、原始容器任务和多种专用插件任务。详见任务类型
  • 软件环境:定义任务容器镜像、依赖项甚至编程语言。详见任务软件环境
  • 硬件环境:定义资源需求(处理器数量、存储量)和机器节点特性(CPU 和 GPU 类型)。详见任务硬件环境

混合匹配任务特性

在这三个维度上,您可以混合匹配特性来构建完全符合需求的任务定义,同时仍可利用工作流层级的所有功能,如输出缓存、版本控制和可重复性。

具有不同特性的任务可以组合到单个工作流中。例如,一个工作流可能包含:

  • 使用默认容器镜像运行的 Python 任务:具有默认依赖项及默认资源和硬件配置
  • 在附加依赖项的容器镜像上运行的 Python 任务:配置在具有特定类型 GPU 的机器节点上运行
  • 运行 Java 进程的原始容器任务
  • 运行 Spark 作业的插件任务:在集群内生成自有子集群
  • map 任务:并行运行多个 Python 任务副本

这种异构任务的组合能力使 Flyte 具有独特的灵活性。

注意:并非所有参数都兼容。例如,对于专用插件任务类型,某些配置不可用(这取决于具体任务插件的细节)。

任务配置

@fl.task 装饰器可接受多个参数来配置任务行为,例如:

  • 指定软件依赖项
  • 硬件要求
  • 缓存行为
  • 重试策略等

更多信息请参阅任务参数

Map 任务

Map 任务允许在单个工作流节点内执行任务的多个实例。通过该功能,您可以跨输入数据集执行任务而无需为每个输入创建独立节点,从而显著提升处理性能。

Map 任务适用于以下典型场景:

  • 多个输入需要运行相同代码逻辑时
  • 需要并行处理多个数据批次时

与常规任务类似,Map 任务在集群资源允许范围内会自动实现最大程度的并行化。

THRESHOLD = 11
    
@fl.task
def detect_anomalies(data_point: int) -> bool:
    return data_point > THRESHOLD
    
@fl.workflow
def map_workflow(data: list[int] = [10, 12, 11, 10, 13, 12, 100, 11, 12, 10]) -> list[bool]:
    # 使用map任务将异常检测函数应用于每个数据点
    return fl.map_task(detect_anomalies)(data_point=data)

Map 任务也支持对启动计划(launch plans)进行映射操作。更多信息和示例代码请参阅启动计划映射

资源配置定制

通过with_overrides方法可自定义单个Map任务的资源分配,例如内存使用量。以下是在工作流中配置detect_anomalies map任务资源参数的示例:

import union
    
@fl.workflow
def map_workflow_with_resource_overrides(
    data: list[int] = [10, 12, 11, 10, 13, 12, 100, 11, 12, 10]
) -> list[bool]:
    return (
        fl.map_task(detect_anomalies)(data_point=data)
        .with_overrides(requests=fl.Resources(mem="2Gi"))
    )

并发控制与成功率配置

Map任务支持以下关键参数配置:

  • concurrency: 限制可并行运行的映射任务数量。若输入规模超过并发值,将按批次串行处理。未指定时表示无限制并发。
  • min_success_ratio: 定义任务标记为成功前必须完成的最小任务比例。
@fl.workflow
def map_workflow_with_additional_params(
    data: list[int] = [10, 12, 11, 10, 13, 12, 100, 11, 12, 10]
) -> list[typing.Optional[bool]]:
    return fl.map_task(
        detect_anomalies, 
        concurrency=1,
        min_success_ratio=0.75
    )(data_point=data)

扩展阅读

更多实践案例请参考unionai-examples代码库中的Map任务示例,以及官方文档的Map任务章节

其他任务类型

任务类型包括:

  • PythonFunctionTask:该Python类表示标准默认任务。使用@fl.task装饰器时创建的就是此类型。
  • ContainerTask:该Python类表示原始容器。允许安装任意镜像,提供对任务的完全控制。
  • Shell任务:用于在Flyte中执行bash脚本。
  • 专用插件任务:包括专用类和PythonFunctionTask的专用配置,实现与第三方系统的集成。

PythonFunctionTask

当使用@fl.task装饰器修饰Python函数时,创建的就是此任务类型。它表示将在单个容器中运行的Python函数。例如:

@fl.task
def get_data() -> pd.DataFrame:
    """获取葡萄酒数据集"""
    return load_wine(as_frame=True).frame

参考Python函数任务示例

这是最常见的任务变体,也是本文档迄今为止重点介绍的类型。

ContainerTask

此任务变体表示原始容器,不对容器内运行的内容做任何预设。以下是声明ContainerTask的示例:

greeting_task = ContainerTask(
    name="echo_and_return_greeting",
    image="alpine:latest",
    input_data_dir="/var/inputs",
    output_data_dir="/var/outputs",
    inputs=kwtypes(name=str),
    outputs=kwtypes(greeting=str),
    command=["/bin/sh", "-c", "echo 'Hello, my name is {{.inputs.name}}.' | tee -a /var/outputs/greeting"],
)

ContainerTask允许在工作流中包含执行任意语言(不仅是Python)代码的任务。

以下示例中的任务计算椭圆面积。名称在整个项目中必须唯一。用户可以指定:

input_data_dir -> 输入数据将被写入的位置

output_data_dir -> Flyte期望输出存在的位置

inputsoutputs指定任务的接口,应为类型化输入/输出变量的有序字典

image字段指定任务的容器镜像(镜像名称或ImageSpec)。要访问镜像未包含的文件,可使用ImageSpec将文件/目录复制到容器/root目录。

通过配置metadata参数中的TaskMetadata,可在ContainerTask中启用缓存:

calculate_ellipse_area_haskell = ContainerTask(
    name="ellipse-area-metadata-haskell",
    input_data_dir="/var/inputs",
    output_data_dir="/var/outputs",
    inputs=kwtypes(a=float, b=float),
    outputs=kwtypes(area=float, metadata=str),
    image="ghcr.io/flyteorg/rawcontainers-haskell:v2",
    command=[
        "./calculate-ellipse-area",
        "{{.inputs.a}}",
        "{{.inputs.b}}",
        "/var/outputs",
    ],
    metadata=TaskMetadata(cache=True, cache_version="1.0"),
)

calculate_ellipse_area_julia = ContainerTask(
    name="ellipse-area-metadata-julia",
    input_data_dir="/var/inputs",
    output_data_dir="/var/outputs",
    inputs=kwtypes(a=float, b=float),
    outputs=kwtypes(area=float, metadata=str),
    image="ghcr.io/flyteorg/rawcontainers-julia:v2",
    command=[
        "julia",
        "calculate-ellipse-area.jl",
        "{{.inputs.a}}",
        "{{.inputs.b}}",
        "/var/outputs",
    ],
    metadata=TaskMetadata(cache=True, cache_version="1.0"),
)

@workflow
def wf(a: float, b: float):
    area_haskell, metadata_haskell = calculate_ellipse_area_haskell(a=a, b=b)
    area_julia, metadata_julia = calculate_ellipse_area_julia(a=a, b=b)

参考容器任务示例

Shell任务

Shell任务支持在Flyte中执行bash脚本。创建Shell任务时需提供名称、待执行的bash脚本,并按需定义输入输出:

示例

from pathlib import Path
from typing import Tuple

import flytekit as fl
from flytekit import kwtypes
from flytekit.extras.tasks.shell import OutputLocation, ShellTask

t1 = ShellTask(
    name="task_1",
    debug=True,
    script="""
    set -ex
    echo "你好!让我们使用shell任务运行一些bash脚本。"
    echo "展示shell任务。" >> {inputs.x}
    if grep "shell" {inputs.x}
    then
        echo "找到匹配!" >> {inputs.x}
    else
        echo "未找到!"
    fi
    """,
    inputs=kwtypes(x=FlyteFile),
    output_locs=[OutputLocation(var="i", var_type=FlyteFile, location="{inputs.x}")],
)

t2 = ShellTask(
    name="task_2",
    debug=True,
    script="""
    set -ex
    cp {inputs.x} {inputs.y}
    tar -zcvf {outputs.j} {inputs.y}
    """,
    inputs=kwtypes(x=FlyteFile, y=FlyteDirectory),
    output_locs=[OutputLocation(var="j", var_type=FlyteFile, location="{inputs.y}.tar.gz")],
)

t3 = ShellTask(
    name="task_3",
    debug=True,
    script="""
    set -ex
    tar -zxvf {inputs.z}
    cat {inputs.y}/$(basename {inputs.x}) | wc -m > {outputs.k}
    """,
    inputs=kwtypes(x=FlyteFile, y=FlyteDirectory, z=FlyteFile),
    output_locs=[OutputLocation(var="k", var_type=FlyteFile, location="output.txt")],
)

ShellTask参数说明:

  • inputs参数允许指定任务接受的输入类型
  • output_locs参数定义输出位置(可以是FlyteFileFlyteDirectory
  • script参数包含实际执行的bash脚本({inputs.x}{outputs.j}等占位符将被实际值替换)
  • debug参数用于调试目的

定义任务实例化FlyteFileFlyteDirectory时,在FlyteDirectory中创建.gitkeep文件作为占位符:

@fl.task
def create_entities() -> Tuple[fl.FlyteFile, fl.FlyteDirectory]:
    working_dir = Path(fl.current_context().working_directory)
    flytefile = working_dir / "test.txt"
    flytefile.touch()

    flytedir = working_dir / "testdata"
    flytedir.mkdir(exist_ok=True)

    flytedir_file = flytedir / ".gitkeep"
    flytedir_file.touch()
    return flytefile, flytedir

创建工作流定义任务依赖关系:

@fl.workflow
def shell_task_wf() -> fl.FlyteFile:
    x, y = create_entities()
    t1_out = t1(x=x)
    t2_out = t2(x=t1_out, y=y)
    t3_out = t3(x=x, y=y, z=t2_out)
    return t3_out

可本地运行工作流:

if __name__ == "__main__":
    print(f"运行shell_task_wf() {shell_task_wf()}")

专用插件任务类与配置

Flyte支持多种插件任务,包括:

  • 专用任务类:直接实现的特定功能
  • @fl.task的专用配置:通过配置PythonFunctionTask实现

这些插件支持:

  • 查询外部数据库(AWS Athena、BigQuery、DuckDB、SQL、Snowflake、Hive)
  • 在Flyte中执行专用处理(虚拟集群中的Spark/Dask、Sagemaker、Airflow、Modin、Ray、MPI和Horovod)
  • 将处理移交给外部服务(AWS Batch、Databricks上的Spark、外部集群的Ray)
  • 数据转换(Great Expectations、DBT、Dolt、ONNX、Pandera)
  • 数据跟踪与展示(MLFlow、Papermill)

参考集成章节获取示例。

任务参数

通过@fl.task装饰器可配置以下任务参数:

  • accelerator: 指定任务使用的硬件加速器。详细信息请参阅指定加速器

  • cache: 缓存配置,详见缓存机制

  • cache_serialize: 缓存序列化配置,详见缓存机制

  • cache_version: 缓存版本控制,详见缓存机制

  • cache_ignore_input_vars: 计算缓存哈希值时应忽略的输入变量列表

  • container_image: 容器镜像配置,参考ImageSpec

  • deprecated: 用于标记已弃用任务的警告信息字符串。空值表示任务处于激活状态

  • docs: 任务说明文档

  • enable_deck: 启用任务执行可视化面板,详见执行面板

@fl.task(enable_deck=True)
def my_task(my_str: str):
    print("hello {my_str}")
  • environment: 环境变量配置,详见环境变量

  • interruptible: 可中断实例配置,详见可中断实例

  • limits: 资源限制配置,详见自定义任务资源

  • node_dependency_hints: 声明任务依赖项列表(适用于动态任务/工作流)。当Flyte无法在运行时前自动确定依赖关系时,可通过此参数显式声明依赖的任务、启动计划或工作流。对于动态调用启动计划的情况尤其重要,因为启动计划需要预先注册才能执行。

@fl.workflow
def workflow0():
    launchplan0 = LaunchPlan.get_or_create(workflow0)
    # 指定node_dependency_hints确保launchplan0能被正确注册
@fl.dynamic(node_dependency_hints=[launchplan0])
def launch_dynamically():
    # 动态调用子启动计划需要确保其已在flyteadmin注册
    return [launchplan0]*10

使用partial为任务参数设置默认值

通过functools.partial可为任务参数设置默认值或固定值:

import functools
import flytekit as fl

@fl.task
def slope(x: list[int], y: list[int]) -> float:
    sum_xy = sum([x[i] * y[i] for i in range(len(x))])
    sum_x_squared = sum([x[i] ** 2 for i in range(len(x))])
    n = len(x)
    return (n * sum_xy - sum(x) * sum(y)) / (n * sum_x_squared - sum(x) ** 2)

@fl.workflow
def simple_wf_with_partial(x: list[int], y: list[int]) -> float:
    partial_task = functools.partial(slope, x=x)
    return partial_task(y=y)

命名输出

Flyte默认使用o1, o2, o3on格式为任务/工作流输出命名。可通过自定义命名提升多输出场景的可读性。

任务输出命名

使用NamedTuple定义任务输出结构:

import flytekit as fl
from typing import NamedTuple

slope_value = NamedTuple("slope_value", [("slope", float)])

@fl.task
def slope(x: list[int], y: list[int]) -> slope_value:
    sum_xy = sum([x[i] * y[i] for i in range(len(x))])
    sum_x_squared = sum([x[i] ** 2 for i in range(len(x))])
    n = len(x)
    return (n * sum_xy - sum(x) * sum(y)) / (n * sum_x_squared - sum(x) ** 2)

工作流输出命名

定义工作流输出结构并显式返回命名元组:

intercept_value = NamedTuple("intercept_value", [("intercept", float)])

@fl.task
def intercept(x: list[int], y: list[int], slope: float) -> intercept_value:
    mean_x = sum(x) / len(x)
    mean_y = sum(y) / len(y)
    intercept = mean_y - slope * mean_x
    return intercept

建议显式声明NamedTuple以避免mypy等工具的lint错误:

def slope() -> NamedTuple("slope_value", slope=float):
    pass

工作流中的解包操作

在工作流中解包命名输出,并返回自定义结构:

slope_and_intercept_values = NamedTuple("slope_and_intercept_values", [("slope", float), ("intercept", float)])


@fl.workflow
def simple_wf_with_named_outputs(x: list[int] = [-3, 0, 3], y: list[int] = [7, 4, -2]) -> slope_and_intercept_values:
    slope_value = slope(x=x, y=y)
    intercept_value = intercept(x=x, y=y, slope=slope_value.slope)
    return slope_and_intercept_values(slope=slope_value.slope, intercept=intercept_value.intercept)

本地运行示例:

if __name__ == "__main__":
    print(f"Running simple_wf_with_named_outputs() {simple_wf_with_named_outputs()}")

启动任务

任务视图
(例如通过在任务列表
中选择任务访问),您可以在右上角选择启动任务

这将打开任务的新建执行对话框:

新建执行

设置项与工作流的类似。顶部可选择:

  • 要启动的此任务特定版本

左侧边栏包含以下部分:

  • 输入参数:任务函数的输入参数在此显示为待填字段

  • 设置

    • 执行名称:本次执行的自定义名称。若未指定,将生成默认名称
    • 覆盖缓存输出:布尔值。设为True时,本次执行将覆盖之前计算的缓存输出
    • 原始输出数据配置:存储原始输出数据的远程路径前缀。默认情况下,工作流输出将写入内置元数据存储。也可在组织、项目域或单个执行级别指定自定义输出位置。此字段用于在工作流执行级别指定该设置,若填写将覆盖更高级别的设置。参数应为可写资源的URL(例如http://s3.amazonaws.com/my-bucket/)。参见原始数据存储
    • 最大并行度:可并行执行的工作流节点数量。若未指定则使用项目/域默认值。设为0表示无限制
    • 强制可中断:用于覆盖本次执行工作流可中断设置的三态选项。未设置时使用工作流的可中断设置。若启用则本次执行使用interruptible=True,若禁用则使用interruptible=False。参见可中断实例
    • 服务账户:本次执行使用的服务账户。未指定时使用默认账户
  • 环境变量:本次工作流执行中任务可用的环境变量

  • 标签:应用于执行资源的标签

  • 通知:为本次工作流执行配置的通知

    • 调试:用于调试目的的工作流执行详细信息

选择启动即可开始任务执行。这将跳转至执行视图

查看任务

任务列表

在侧边栏选择 Tasks 将显示所有已注册任务的列表:

任务列表
您可以通过名称搜索任务,并筛选仅显示已归档的任务。

列表中每个任务都会显示以下基本信息:

  • Inputs: 任务的输入类型
  • Outputs: 任务的输出类型
  • Description: 任务的描述

选择列表中的条目可跳转到具体任务视图

任务视图

任务列表中选择单个任务将进入任务视图:

任务视图

在此界面可查看:

  • Inputs & Outputs: 任务的输入输出类型
  • 最近的任务版本(选择任意版本将跳转至任务版本视图
  • 该任务的最近执行记录(选择任意记录将跳转至执行视图

任务版本列表

任务版本列表提供特定任务版本的详细信息:

任务版本列表

  • Image: 用于运行该任务的 Docker 镜像
  • Env Vars: 该任务使用的环境变量
  • Commands: 定义该任务的 JSON 对象

底部显示任务的所有版本列表,当前选中版本会高亮显示。

任务软件环境

@fl.task装饰器提供以下参数来指定任务运行的软件环境:

本地镜像构建

Flyte 中的每个工作流任务都在其专属容器中运行。由于容器需要容器镜像才能启动,因此 Flyte 中的每个任务都必须关联一个容器镜像。您可以通过定义 ImageSpec 对象并将其传递给 @fl.task 装饰器的 container_image 参数来指定任务使用的容器镜像。当注册工作流时,容器镜像将在本地构建并推送到指定的容器注册表。工作流执行时,系统会从该注册表中拉取镜像用于任务运行。

完整 ImageSpec 类的参数和方法说明请参阅 ImageSpec API 文档

以下通过示例说明具体操作流程。

项目结构

├── requirements.txt
└── workflows
    ├── __init__.py
    └── imagespec-simple-example.py

requirements.txt

union
pandas

imagespec-simple-example.py

import typing
import pandas as pd
import flytekit as fl

image_spec = union.ImageSpec(
    registry="ghcr.io/<my-github-org>",
    name="simple-example-image",
    base_image="ghcr.io/flyteorg/flytekit:py3.11-latest",
    requirements="requirements.txt"
)

@fl.task(container_image=image_spec)
def get_pandas_dataframe() -> typing.Tuple[pd.DataFrame, pd.Series]:
    df = pd.read_csv("https://storage.googleapis.com/download.tensorflow.org/data/heart.csv")
    print(df.head())
    return df[["age", "thalach", "trestbps", "chol", "oldpeak"]], df.pop("target")

@fl.workflow()
def wf() -> typing.Tuple[pd.DataFrame, pd.Series]:
    return get_pandas_dataframe()

安装并配置 pyflyte 和 Docker

Docker 安装指南请参考 容器镜像处理设置pyflyte 连接 Flyte 实例的配置方法请参阅 入门指南

设置镜像注册表

需要准备一个可供 Flyte 在执行任务时拉取镜像的容器注册表。可使用 Docker Hub 或 GitHub Container Registry 等公共注册表,也可使用 AWS Elastic Container Registry (ECR) 或 Google Artifact Registry (GAR) 等组织内部注册表。

所选注册表必须满足以下条件:

  • 对执行工作流的 Flyte 实例可访问
  • 推送至注册表的镜像需设置为公开可访问

本示例使用 GitHub 的 ghcr.io 容器注册表,更多信息请参阅 容器注册表操作指南

注册表身份验证

需配置本地 Docker 客户端以通过 GHCR 认证。这是为了让 pyflyte CLI 能将根据 ImageSpec 构建的镜像推送到 GHCR。

具体操作请遵循 容器注册表操作指南 > 容器注册表身份验证

在 Flyte 上设置项目与域

需要在 Flyte 实例上创建项目用于注册工作流,操作指南请参考 项目设置

依赖项说明

requirements.txt 文件包含任务所需的 flytekitpandas 包。

创建 Python 虚拟环境

在项目根目录执行 pip install -r requirements.txt 安装依赖项。

本地运行工作流

在项目根目录执行:pyflyte run workflows/imagespec-simple-example.py wf。更多细节请参考 代码运行指南

注意:本地 Python 环境中运行工作流时不会构建或推送镜像(实际上完全不使用容器镜像)。

注册工作流

在项目根目录执行:

pyflyte register workflows/imagespec-simple-example.py

pyflyte 将构建容器镜像并推送到 ImageSpec 指定的注册表,随后将工作流注册到 Flyte。注册完成后,可通过 UI 在指定项目和域中查看已注册的工作流。

确保镜像公开可访问

使用 ghcr.io 注册表时,必须将容器镜像可见性设置为 Public 才能在 Flyte 上运行工作流。具体操作请参考 配置软件包访问控制与可见性

在 Flyte 上运行工作流

确认镜像已公开后,点击 Launch Workflow 即可运行工作流。

重要访问性验证
若工作流使用私有容器镜像或存在访问限制,系统将返回错误:

... Failed to pull image ...
... Error: ErrImagePull
... Back-off pulling image ...
... Error: ImagePullBackOff

多镜像工作流

同一工作流中可为不同任务指定不同镜像。当部分任务依赖项与其他任务存在显著差异时,此功能特别有用。

以下示例展示两个任务:

  • CPU 任务:使用 flytekit 默认镜像
  • GPU 任务:使用预构建镜像(支持 Kubeflow Pytorch 分布式训练)
import numpy as np
import torch.nn as nn

@task(
    requests=Resources(cpu="2", mem="16Gi"),
    container_image="ghcr.io/flyteorg/flytekit:py3.9-latest",
)
def get_data() -> Tuple[np.ndarray, np.ndarray]:
    ...  # 以 numpy 多维数组形式获取数据集


@task(
    requests=Resources(cpu="4", gpu="1", mem="16Gi"),
    container_image="ghcr.io/flyteorg/flytecookbook:kfpytorch-latest",
)
def train_model(features: np.ndarray, target: np.ndarray) -> nn.Module:
    ...  # 使用 GPU 训练模型

环境变量

environment 参数允许您指定需要在任务容器执行环境中存在的变量值。例如:

@fl.task(environment={"MY_ENV_VAR": "my_value"})
def my_task() -> str:
    return os.environ["MY_ENV_VAR"]

该参数接受字典格式的键值对,其中:

  • 键(Key):环境变量名称(字符串类型)
  • 值(Value):环境变量对应的字符串值

在任务函数内部,可以通过标准库的os.environ字典访问这些环境变量。示例中MY_ENV_VAR的值将被设置为"my_value",并在任务执行时通过os.environ["MY_ENV_VAR"]读取。

查看日志

执行视图中,从Nodes标签页的任务列表中选择任务后,右侧面板将显示任务详细信息。

在该面板的Execution标签页下,Logs区域会显示一个标记为Task Logs的链接。

任务日志链接

点击该链接将跳转至执行详情页面Execution logs标签页

执行日志

执行日志提供任务运行时标准输出的实时视图。

例如,任务Python代码中的任何print语句都会在此处显示。

Kubernetes集群日志

页面左侧区域还可以查看任务执行对应的Kubernetes集群日志:

Kubernetes集群日志

其他标签页

执行详情页面中,除Execution logs标签页外,还可以找到Execution resourcesInputs & Outputs标签页。

云服务商日志

Task Logs链接旁,还会显示对应云服务商的日志链接(AWS的Cloudwatch Logs、GCP的Stackdriver Logs、Azure的Azure Logs):

云服务商日志链接

如果已登录云服务商账户并具备适当权限,该链接将直接跳转到运行当前任务的特定容器日志页面。

引用任务

reference_task 用于引用已经定义、序列化并注册的任务。您可以引用其他项目中的任务,创建使用他人声明任务的工作流。这些任务可以存在于独立的容器环境、Python 运行时环境、不同版本的flytekit,甚至支持不同的编程语言。

引用任务无法在本地运行。如需本地测试,请模拟这些任务。

示例

  1. 创建名为 task.py 的文件并插入以下内容:

    import flytekit as fl
    
    @fl.task
    def add_two_numbers(a: int, b: int) -> int:
        return a + b
    
  2. 注册任务:

    pyflyte register --project flytesnacks --domain development --version v1 task.py
    
  3. 创建独立文件 wf_ref_task.py 并复制以下代码:

    from flytekit import reference_task
    
    @reference_task(
        project="flytesnacks",
        domain="development",
        name="task.add_two_numbers",
        version="v1",
    )
    def add_two_numbers(a: int, b: int) -> int:
        ...
    
    @fl.workflow
    def wf(a: int, b: int) -> int:
        return add_two_numbers(a, b)
    
  4. 注册 wf 工作流:

    pyflyte register --project flytesnacks --domain development wf_ref_task.py
    
  5. 在 Flyte UI 中运行工作流 wf_ref_task.wf

任务硬件环境

自定义任务资源

您可以自定义任务代码执行时的硬件环境。

根据需求,有两种不同的方式来定义和注册具有自定义硬件要求的任务:

  • @fl.task装饰器中配置
  • 定义PodTemplate

使用@fl.task装饰器

您可以指定以下资源的requestslimits

  • CPU数量
  • GPU数量
  • 内存大小
  • 临时存储大小

详见自定义任务资源

使用PodTemplate

如果需要更复杂的配置,可以使用Kubernetes级别的配置来约束任务仅在特定机器类型上运行。

这要求您提前设置所需的机器类型和节点组,并配置适当的节点分配设置(节点选择器标签、节点亲和性、污点、容忍度等)。

在任务定义中,您需要使用与节点分配配置匹配的PodTemplate,以确保任务仅被调度到合适的机器类型上。

@fl.task参数:pod_templatepod_template_name

pod_template参数可用于向任务提供自定义的KubernetesPodTemplate,用于定义节点选择器、亲和性、容忍度等Kubernetes特有设置。

pod_template_name是相关参数,用于指定已存在的PodTemplate资源名称,该资源将被用于当前任务。

详见使用Kubernetes PodTemplate配置任务Pod

加速器

如果指定GPU,还可以通过设置accelerator参数来指定要使用的GPU类型。详见加速器获取更多信息。

任务级监控

您还可以监控任务使用的硬件资源。详见任务级监控

自定义任务资源

在定义任务函数时,可以为运行任务的 pod 指定资源需求。Flyte 将根据这些需求确保任务 pod 被调度到符合指定资源配置的 Kubernetes 节点上运行。

资源规格定义

资源规格通过 @fl.task 装饰器指定。示例如下:

from flytekit.extras.accelerators import A100

@fl.task(
    requests=Resources(mem="120Gi", cpu="44", gpu="8", ephemeral_storage="100Gi"),
    limits=Resources(mem="200Gi", cpu="100", gpu="12", ephemeral_storage="200Gi"),
    accelerator=GPUAccelerator("nvidia-tesla-a100")
)
def my_task()
    ...

资源相关配置分为三个独立部分:

  • requests
  • limits
  • accelerator

requests 与 limits 配置

requestslimits 参数各接收一个 Resource 对象,该对象包含五个可选属性:

  • cpu: CPU 核心数(整数或毫核单位 m
  • gpu: GPU 核心数(整数或毫核单位 m
  • mem: 主内存(单位 Mi, Gi 等)
  • ephemeral_storage: 临时存储(单位 Mi, Gi 等)

注意:

  • CPU 和 GPU 分配可使用整数或毫核单位。例如 cpu="2500m" 表示 2.5 个 CPU 核心,gpu="3000m" 表示 3 个 GPU 核心
  • requests 表示任务运行所需最低资源,确保 pod 只调度到满足该配置的节点
  • limits 表示资源使用的硬性上限,任务不会调度到超过该配置的节点

GPU 资源注意事项
GPU 资源应仅在 limits 部分指定:

  • Kubernetes 会将 limits 值自动作为 requests
  • 可以同时在 limitsrequests 中指定 GPU,但两者必须相等
  • 不可在 requests 中单独指定 GPU 而不在 limits 中指定

accelerator 配置

accelerator 参数用于指定任务所需的专用硬件类型,可以是:

  • GPU 型号(如 A100
  • 部分 GPU 资源
  • 其他硬件设备(如 TPU)

详见加速器文档

执行默认值与资源配额

在 Dashboard 右侧边栏可查看执行默认值与资源配额。点击齿轮图标进行编辑:

execution-defaults-gear

编辑对话框如下:

execution-defaults-dialog

临时存储注意事项
临时存储默认值 0 表示任务 pod 按需使用节点存储。如果节点存储不足可能导致 pod 被驱逐。建议显式指定临时存储请求以避免驱逐问题。

任务资源验证

当工作流包含无法满足的资源请求时,执行会立即失败(而不是无限排队等待)。需确保:

  • 集群中物理存在对应节点类型(需正确配置 Flyte 集群部署)
  • 任务装饰器中正确指定节点要求(通过 requests, limits, accelerator 等参数)

with_overrides 方法

当在 @fl.task 装饰器中指定 requests, limitsaccelerator 时,这些配置将应用于所有工作流调用。如需在不同调用中修改资源配置,可使用任务函数的 with_overrides 方法

示例:

@fl.task
def my_task(ff: FlyteFile):
    ...

@fl.workflow
def my_workflow():
    my_task(ff=smallFile)
    my_task(ff=bigFile).with_overrides(requests=Resources(mem="120Gi", cpu="10"))

加速器

Flyte允许为任务指定可用GPU数量的requests和limits。但在某些情况下,可能需要更具体地指定要使用的GPU类型或其他专用设备类型。

您可以使用accelerator参数来指定具体的GPU类型、GPU变体、分片GPU或其他专用硬件设备(如TPU)。

每个设备类型都有常量名称,可用于在accelerator参数中指定设备。例如:

from flytekit.extras.accelerators import A100

    @fl.task(
        limits=Resources(gpu="1"),
        accelerator=A100,
    )
    def my_task():
        ...

使用预定义加速器常量

flytekit.extras.accelerators模块中提供了多个预定义的加速器常量。

预定义列表并未包含所有加速器,但包含了最常见的类型。如果您知道加速器名称但找不到预定义常量,可以直接将字符串名称传递给任务装饰器。

注意:要使特定加速器在Flyte安装中可用,必须已在Flyte集群部署时预先配置该设备(参见部署文档)。

使用常量时可直接从模块导入,例如:

from flytekit.extras.accelerators import T4

@fl.task(
    limits=Resources(gpu="1"),
    accelerator=T4,
)
def my_task():
    ...

如需使用分片GPU,可在加速器常量上使用partitioned方法,例如:

from flytekit.extras.accelerators import A100

@fl.task(
    limits=Resources(gpu="1"),
    accelerator=A100.partition_2g_10gb,
)
def my_task():
    ...

预定义加速器常量列表

  • A10G: NVIDIA A10 Tensor Core GPU
  • L4: NVIDIA L4 Tensor Core GPU
  • K80: NVIDIA Tesla K80 GPU
  • M60: NVIDIA Tesla M60 GPU
  • P4: NVIDIA Tesla P4 GPU
  • P100: NVIDIA Tesla P100 GPU
  • T4: NVIDIA T4 Tensor Core GPU
  • V100 NVIDIA Tesla V100 GPU
  • A100: 完整NVIDIA A100 GPU,支持分片配置:
    • A100.partition_1g_5gb: A100 GPU的5GB分片
    • A100.partition_2g_10gb: A100 GPU的10GB分片 - 2个5GB切片,含2/7的SM(流式多处理器)
    • A100.partition_3g_20gb: A100 GPU的20GB分片 - 4个5GB切片,含3/7的SM
    • A100.partition_4g_20gb: A100 GPU的20GB分片 - 4个5GB切片,含4/7的SM
    • A100.partition_7g_40gb: A100 GPU的40GB分片 - 8个5GB切片,含7/7的SM
  • A100_80GB: 完整NVIDIA A100 80GB GPU,支持分片配置:
    • A100_80GB.partition_1g_10gb: A100 80GB GPU的10GB分片 - 2个5GB切片,含1/7的SM
    • A100_80GB.partition_2g_20gb: A100 80GB GPU的20GB分片 - 4个5GB切片,含2/7的SM
    • A100_80GB.partition_3g_40gb: A100 80GB GPU的40GB分片 - 8个5GB切片,含3/7的SM
    • A100_80GB.partition_4g_40gb: A100 80GB GPU的40GB分片 - 8个5GB切片,含4/7的SM
    • A100_80GB.partition_7g_80gb: A100 80GB GPU的80GB分片 - 16个5GB切片,含7/7的SM

有关分片的更多信息,请参考分片GPU文档

重试与超时

重试类型

Flyte 允许自动重试失败任务。本节将解释重试的配置和应用方法。

导致任务失败的错误分为两种主要类型,其重试逻辑有所不同:

  • SYSTEM(系统错误):由基础设施相关故障引起,例如硬件故障或网络问题。这类错误通常是暂时性的,通常可以通过重试解决。

  • USER(用户错误):由用户定义代码中的问题导致,例如值错误或逻辑错误,这类错误通常需要修改代码才能解决。

配置重试

Flyte 支持针对 USERSYSTEM 错误进行重试配置,允许定制容错策略:

USER 错误处理
可通过任务装饰器的 retries 属性设置重试次数,直接在任务定义中控制:

import random
from flytekit import task

@task(retries=3)
def compute_mean(data: List[float]) -> float:
    if random() < 0.05:
        raise FlyteRecoverableException("发生严重错误 🔥")  # 仅翻译注释部分
    return sum(data) / len(data)

SYSTEM 错误管理
通过 FlytePropeller 配置中的 max-node-retries-system-failures 等平台级设置进行管理,无需修改任务代码即可实现重试控制。

此外,node-config 键中的 interruptible-failure-threshold 选项定义了可中断的系统级重试次数。该配置特别适用于在可抢占实例上运行的任务。

更多详细信息请参考 Flyte Propeller 配置文档

可中断任务重试

标记为可中断的任务被抢占后重试时,不会消耗 USER 错误的重试次数预算。该特性特别适用于使用可抢占计算资源(如 spot 实例)运行的任务。

详见:可中断实例

Map 任务重试

对于 map 任务,可中断行为与常规任务保持一致。任务注解中的 retries 字段不用于处理 SYSTEM 错误(这些错误由平台配置管理),而 USER 错误的重试次数预算仍通过任务装饰器的 retries 参数设置。

详见:Map 任务

超时设置

为防止因系统级问题导致的僵尸任务挂起,可通过任务装饰器的 timeout 参数设置最大运行时间。

以下示例确保任务在运行超过 1 小时后自动终止:

from datetime import timedelta

@task(timeout=timedelta(hours=1))
def compute_mean(data: List[float]) -> float:
    return sum(data) / len(data)

注意:timeout 参数接受 Python 内置的 timedelta 对象作为输入参数。

可中断实例

在 AWS 中,使用术语 spot instance。在 GCP 中,等效术语是 spot VM(即预emptible VM)。本文中我们将统一使用 可中断实例 这个通用术语来描述这两个云提供商的此类实例。

可中断实例是云提供商提供给集群的机器实例,不保证始终可用。因此,可中断实例比常规实例更便宜。要将可中断实例用于计算工作负载,您必须做好以下准备:由于资源不可用可能导致任务执行失败,此时需要重试操作。

部署 Flyte 集群时,可以选择是否使用可中断实例。

对于每个指定的可中断实例节点组,我们建议配置一个附加的按需节点组(在其他配置方面与可中断节点组完全相同)。当可中断实例上的任务执行失败时,这个按需节点组将作为备用方案使用。

配置任务使用可中断实例

要在可中断实例上调度任务并在失败时重试,请在 @fl.task 装饰器中指定 interruptibleretries 参数。例如:

@fl.task(interruptible=True, retries=3)
  • 仅当任务包含参数 interruptible=True(或其所属工作流包含 interruptible=True 参数且任务本身未显式设置 interruptible 参数)时,任务才会被调度到可中断实例
  • 可中断任务与其他任务一样,可以设置 retries 参数
  • 如果可中断任务未显式设置 retries 参数,则 retries 默认值为 1
  • 设置 retries=n 的可中断任务将在可中断实例上尝试 n 次。如果 n 次尝试后仍失败,最后一次(第 n+1 次)重试将在备用的按需实例上执行

工作流级别的可中断配置

可中断配置也可在工作流级别设置。工作流级别的配置将应用于所有未显式设置该参数的任务。任务级别的可中断配置始终会覆盖工作流级别的设置。

可中断实例的优缺点

使用可中断实例执行任务的主要优势是成本低于按需实例(其他参数相同的情况下)。但存在两个主要缺点:

  1. 任务成功调度到可中断实例后被中断:最坏情况下,对于 retries=n 的任务,可能会被中断 n 次,直到最终使用备用的按需实例。显然,这对时间敏感型任务会造成问题

  2. 所选节点类型的可中断实例在首次调度时不可用:这种情况可能导致任务无限期挂起,直到可中断实例可用。注意这与前一种故障模式不同——前一种情况是成功调度后发生中断

总体建议:在资源可用时尽量使用可中断实例,但仅适用于非时间敏感型任务。

风险提示与免责声明
本文内容基于公开信息研究整理,不构成任何形式的投资建议。历史表现不应作为未来收益保证,市场存在不可预见的波动风险。投资者需结合自身财务状况及风险承受能力独立决策,并自行承担交易结果。作者及发布方不对任何依据本文操作导致的损失承担法律责任。市场有风险,投资须谨慎。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

船长Q

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值