dflow入门2——Slices

为了梳理学习dflow时遇到的知识点,我决定开这一个系列记录自己的学习过程。当然了,最好是去看 官方教程文档
本文,我们将阅读教程中slices这一节,并在最后写一个应用。

阅读原教程

两个OP

我们首先看源码中的第一个OP

from typing import List

from dflow import Step, Workflow, argo_range
from dflow.python import OP, OPIO, Artifact, OPIOSign, PythonOPTemplate, Slices


class Hello(OP):
    def __init__(self):
        pass

    @classmethod
    def get_input_sign(cls):
        return OPIOSign({
            'filename': str
        })

    @classmethod
    def get_output_sign(cls):
        return OPIOSign({
            'out_art': Artifact(str)
        })

    @OP.exec_sign_check
    def execute(
            self,
            op_in: OPIO,
    ) -> OPIO:
        file_num = int(op_in["filename"].split('.')[0][1:])
        open(op_in["filename"], "w").write("Hello" * file_num)
        op_out = OPIO({
            'out_art': op_in["filename"]
        })
        return op_out

结合上文,我们知道,所谓OP就是一个操作手册,撰写OP需要遵循特定的模板,而该模板可以简化为下面这个普通的python函数。

def aaa(in_msg: str, in_artifact: Artifact):
	op_in = {"in_msg": in_msg, "in_artifact": in_artifact}
	op_out = execute(op_in)
	out_msg, out_artifact = op_out["out_msg"], op_out["out_artifact"]
	return out_msg, out_artifact

我们对照这两个代码块,首先读前两个类方法:hello OP的输入是filename,输出是Artifact类型,初始化该类型需要一个str
下面我们看execute,首先是接收了文件名,然后从文件名中获取file_num,然后新建一个文件,往里写Hello,并重复file_num次,最后把该文件名对应的文件作为输出
结合下文,filename的形式是这样的:

"filename": [f"f{x}.txt" for x in range(10)]

单个的filename是这样的:

f"f{x}.txt" for x in range(10)

所以x是一个数字

f"f{x}.txt"

execute第一句话就是对该文件名进行处理

op_in["filename"].split('.')[0][1:]

下面我们看第二个OP

class Check(OP):
    def __init__(self):
        pass

    @classmethod
    def get_input_sign(cls):
        return OPIOSign({
            'filename': Artifact(List[str])
        })

    @classmethod
    def get_output_sign(cls):
        return OPIOSign()

    @OP.exec_sign_check
    def execute(
            self,
            op_in: OPIO,
    ) -> OPIO:
        print(op_in["filename"])
        return OPIO()

再重复一下
所谓OP就是一个操作手册,撰写OP需要遵循特定的模板,而该模板可以简化为下面这个普通的python函数。

def aaa(in_msg: str, in_artifact: Artifact):
	op_in = {"in_msg": in_msg, "in_artifact": in_artifact}
	op_out = execute(op_in)
	out_msg, out_artifact = op_out["out_msg"], op_out["out_artifact"]
	return out_msg, out_artifact

我们对照这两个代码块看这个OP
该OP输入是Artifact类型,初始化该类型,需要往Artifact里塞一个list,list里面是文件路径
也就是说,输入是大包裹,里面有一群小包裹
输出是一个空的,啥都没有
execute就是把输入的包裹打印出来

OP的使用:slice功能

该功能是template附属的功能,如果说,template是工人,OP是操作手册,那么slice就相当于:工人重复某个操作手册n次(并行)。
下面是使用方法:

hello = Step("hello",
            PythonOPTemplate(Hello, image="python:3.8",
                            slices=Slices("{{item}}",
                                        input_parameter=["filename"],
                                        output_artifact=["out_art"]
                                        )
                            ),
            parameters={"filename": [f"f{x}.txt" for x in range(10)]},
            with_param=argo_range(10))

这里的item应该是一个全局变量,应该是slice中的某个item,是一个int
在这里插入图片描述
整个step的输入:

parameters={"filename": [f"f{x}.txt" for x in range(10)]}

slice中每步的输入输出是:

input_parameter=["filename"],
output_artifact=["out_art"]

当然了,还可以是其他输入输出,下面是slice的源码:
在这里插入图片描述
之所以选择上面两个,是因为我们的hello OP只需要这两个
下面我们对用check step 检查hello OP的结果

check = Step("check",
            PythonOPTemplate(Check, image="python:3.8"),
            artifacts={"filename": hello.outputs.artifacts["out_art"]},
            )

可以看到,该OP接收的是单个的artifacts,结合check OP的定义,我们看到,这是一个大包裹,里面是一堆小包裹,而hello OP的step输出就是一堆小包裹。

工作流

wf = Workflow("slices")
wf.add(hello)
wf.add(check)
wf.submit()

我们定义好了两个OP,然后定义了使用两个OP的step,最后将他们组装即可。

浅试Slice功能

问题阐述

我们有6个xyz文件在in_dir目录里
在这里插入图片描述
每个文件第二行有能量信息
在这里插入图片描述
我们需要做的事情是,打开文件,读取能量数据。将能量数据和文件名称、文件路径对应起来,存到同一个dataframe里面。

传统的脚本

import os
import pandas as pd


root = r'H:\dflow\dflow_gym\dummy_slice\in_dir'
dump = r'H:\dflow\dflow_gym\dummy_slice'
e_list = []
name_list = []
path_list = []
for a_file in os.listdir(root):
    a_path = os.path.join(root, a_file)
    with open(a_path, 'r') as f:
        f.readline()
        a_e = float((f.readline()).split()[1])
    e_list.append(a_e)
    name_list.append(a_file)
    path_list.append(a_path)
info = {'e': e_list, 'name': name_list, 'xyz_path': path_list}
info_df = pd.DataFrame(info)
os.chdir(dump)
info_df.to_pickle('info.pickle')

我们把这段脚本打包成一个函数:
(涉及路径的时候加上path,更规范,不容易出错)

import os
import pandas as pd
from pathlib import Path

def get_e(src_root: str, dump_root: str):
    e_list = []
    name_list = []
    path_list = []
    for a_file in os.listdir(Path(src_root)):
        a_path = os.path.join(Path(src_root), a_file)
        with open(a_path, 'r') as f:
            f.readline()
            a_e = float((f.readline()).split()[1])
        e_list.append(a_e)
        name_list.append(a_file)
        path_list.append(a_path)
    info = {'e': e_list, 'name': name_list, 'xyz_path': path_list}
    info_df = pd.DataFrame(info)
    os.chdir(Path(dump_root))
    info_df.to_pickle('info.pickle')

dflow v1–non slice

下面我们把这段函数改成一个template,外包step和workflow
我们对照一下template的模板:

def aaa(in_msg: str, in_artifact: Artifact):
	op_in = {"in_msg": in_msg, "in_artifact": in_artifact}
	op_out = execute(op_in)
	out_msg, out_artifact = op_out["out_msg"], op_out["out_artifact"]
	return out_msg, out_artifact

运行该函数不需要指令,需要包裹,即in_artifact
该函数没有输出指令,但是输出了一个包裹info.pickle,我们需要把info.pickle下载下来。

流程大概是,我们上传in_dir,在云端处理信息以后,把info.pickle下载到指定文件夹。
我们“借鉴”一下源码中example里面的文件上传和下载,相对路径即可。
下面是dflow第一版:

import os
from pathlib import Path
from dflow import Step, Workflow, download_artifact, upload_artifact
from dflow.python import (OP, OPIO, Artifact, OPIOSign, PythonOPTemplate,
                          upload_packages)
import pandas as pd

if "__file__" in locals():
    upload_packages.append(__file__)


class step1(OP):
    def __init__(self):
        pass

    @classmethod
    def get_input_sign(cls):
        return OPIOSign({
            'in_dir': Artifact(Path),
        })

    @classmethod
    def get_output_sign(cls):
        return OPIOSign({
            'info': Artifact(Path),
        })

    @OP.exec_sign_check
    def execute(
            self,
            op_in: OPIO,
    ) -> OPIO:
        e_list = []
        name_list = []
        path_list = []
        cwd_ = os.getcwd()
        os.chdir(op_in['in_dir'])
        for a_file in os.listdir('./'):
            with open(a_file, 'r') as f:
                f.readline()
                a_e = float((f.readline()).split()[1])
            e_list.append(a_e)
            name_list.append(a_file)
            path_list.append(os.path.abspath(a_file))
        info = {'e': e_list, 'name': name_list, 'xyz_path': path_list}
        info_df = pd.DataFrame(info)
        os.chdir(cwd_)
        info_df.to_pickle(r'info.pickle')
        op_out = OPIO({
            "info": Path(r'info.pickle'),
        })
        return op_out

对比二者不难发现,其实就是把程序输入换成了op_in['in_dir'],输出给套了个壳。
下面我们给这个OP套上template,step,workflow的壳。


def test_dflow_v1():
    wf = Workflow(name='test1')
    art_in = upload_artifact(Path('in_dir'))
    print(art_in)
    step = Step(name='step1',
                template=PythonOPTemplate(Parse, image="python_diy:3.8"),
                artifacts={'in_dir': art_in})
    wf.add(step)
    wf.submit()
    while wf.query_status() in ["Pending", "Running"]:
        time.sleep(1)

    assert(wf.query_status() == "Succeeded")
    step = wf.query_step(name="step1")[0]
    assert(step.phase == "Succeeded")
    download_artifact(artifact=step.outputs.artifacts["info"])


if __name__ == '__main__':
    test_dflow_v1()

程序运行的时候会遇到如下几个问题:

  1. workflow和step的命名,不能有下划线,可以有中短线,而且不能大写开头
  2. 程序运行需要pandas库,直接upload会出现版本兼容问题,最好在镜像里提前打好,方法参见这篇
    上述这种实现是顺序读取列表
    在这里插入图片描述
    我们只调用了一次step1就完成了3个文件的处理。
    下面我们用slice功能

dflow v2–slice_para

使用slice功能,我们需要把OP功能缩减成只读取单个文件的功能,然后重复调用。
因此我们需要调整OP的输入为:单个文件
输出为:e, name, path

import os
from pathlib import Path
import pandas as pd
from dflow import Step, Workflow, download_artifact, upload_artifact
from dflow.python import (OP, OPIO, Artifact, OPIOSign, PythonOPTemplate,
                          upload_packages)
import time


if "__file__" in locals():
    upload_packages.append(__file__)


class getInfo(OP):
    def __init__(self):
        pass

    @classmethod
    def get_input_sign(cls):
        return OPIOSign({
            'in_f': Artifact(Path),
        })

    @classmethod
    def get_output_sign(cls):
        return OPIOSign({
            'e': float,
            'name': str,
            'xyz_path': Path,
        })

    @OP.exec_sign_check
    def execute(
            self,
            op_in: OPIO,
    ) -> OPIO:
        with open(op_in['in_f'], 'r') as f:
            f.readline()
            a_e = float((f.readline()).split()[1])
        op_out = OPIO({
            "e": a_e,
            'name': os.path.basename(op_in['in_f']),
            'xyz_path':os.path.abspath(op_in['in_f'])
        })
        return op_out

另一方面,我们需要汇总所有的信息到 df 里面,所以增加一个OP

class getDF(OP):
    def __init__(self):
        pass

    @classmethod
    def get_input_sign(cls):
        return OPIOSign({
            'in_es': List[float],
            'in_names': List[str],
            'in_paths': List[Path]
        })

    @classmethod
    def get_output_sign(cls):
        return OPIOSign({
            'out_df': Artifact(Path)
        })

    @OP.exec_sign_check
    def execute(
            self,
            op_in: OPIO,
    ) -> OPIO:
        info = pd.DataFrame({'e': op_in['in_es'], 'names': op_in['in_names'], 'xyz_paths': op_in['in_paths']})
        info.to_pickle('info_v2.pickle')
        op_out = OPIO({
            "out_df": Path('info_v2.pickle'),
        })
        return op_out

这两个OP功能加起来是原OP的功能
下面我们把壳套上,照着模板写:

def test_dflow_v2():
    wf = Workflow(name='test1')
    art_in = upload_artifact(Path('in_dir'))
    print(art_in)
    step1 = Step(name='step1',
                template=PythonOPTemplate(getInfo, image="python_diy:3.8", slices=Slices(
                    "{{item}}",
                    input_parameter=['in_f'],
                    output_parameter=['e', 'name', 'xyz_path']
                )),
    parameters = {'in_f': os.listdir(Path('in_dir'))},
                 artifacts={'in_dir': art_in},
                with_param = argo_range(len(os.listdir(Path('in_dir')))),
                key='get-e-{{item}}'
    )
    step2 = Step(name='step2',
                 template=PythonOPTemplate(getDF, image='python_diy:3.8'),
                 parameters={'in_es': step1.outputs.parameters['e'],
                             'in_names': step1.outputs.parameters['name'],
                             'in_paths': step1.outputs.parameters['xyz_path']})
    wf.add(step1)
    wf.add(step2)
    wf.submit()
    while wf.query_status() in ["Pending", "Running"]:
        time.sleep(1)

    assert(wf.query_status() == "Succeeded")
    step = wf.query_step(name="step2")[0]
    assert(step.phase == "Succeeded")
    download_artifact(artifact=step.outputs.artifacts['out_df'])


if __name__ == '__main__':
    test_dflow_v2()

运行以后即为下图所示:
在这里插入图片描述

dflow v3–slice_artifact

注意到,上文在slice的时候是对parameter进行的slice,每个子节点都传了所有的文件。
但是我们理想状态是对文件进行slice,每个子任务处理一个文件,因此只需给OP传一个文件即可。
这个似乎没有很好的方法,只能在上传包裹的时候就完成切片,也就是上传一堆小包裹

import os
from pathlib import Path
import pandas as pd
from dflow import Step, Workflow, download_artifact, upload_artifact, argo_range
from dflow.python import (OP, OPIO, Artifact, OPIOSign, PythonOPTemplate, Slices,
                          upload_packages)
import time
from typing import List


if "__file__" in locals():
    upload_packages.append(__file__)


class getInfo(OP):
    def __init__(self):
        pass

    @classmethod
    def get_input_sign(cls):
        return OPIOSign({
            'in_f': Artifact(Path)
        })

    @classmethod
    def get_output_sign(cls):
        return OPIOSign({
            'e': float,
            'name': str,
            'xyz_path': str,
        })

    @OP.exec_sign_check
    def execute(
            self,
            op_in: OPIO,
    ) -> OPIO:
        with open(op_in['in_f'], 'r') as f:
            f.readline()
            a_e = float((f.readline()).split()[1])
        op_out = OPIO({
            "e": a_e,
            'name': os.path.basename(op_in['in_f']),
            'xyz_path': os.path.abspath(op_in['in_f'])
        })
        return op_out


class getDF(OP):
    def __init__(self):
        pass

    @classmethod
    def get_input_sign(cls):
        return OPIOSign({
            'in_es': List[float],
            'in_names': List[str],
            'in_paths': List[str]
        })

    @classmethod
    def get_output_sign(cls):
        return OPIOSign({
            'out_df': Artifact(Path)
        })

    @OP.exec_sign_check
    def execute(
            self,
            op_in: OPIO,
    ) -> OPIO:
        info = pd.DataFrame({'e': op_in['in_es'], 'names': op_in['in_names'], 'xyz_paths': op_in['in_paths']})
        info.to_pickle('info_v2.pickle')
        op_out = OPIO({
            "out_df": Path('info_v2.pickle'),
        })
        return op_out


def test_dflow_v3():
    wf = Workflow(name='test1')
    upload_list = []
    for a_file in os.listdir('in_dir'):
        upload_list.append(Path(Path('in_dir')/a_file))
    art_in = upload_artifact(upload_list)
    print(art_in)

    step1 = Step(name='step1',
                 template=PythonOPTemplate(getInfo, image="python_diy:3.8", slices=Slices(
                     "{{item}}",
                     input_artifact=['in_f'],
                     output_parameter=['e', 'name', 'xyz_path']
                 )),
                 artifacts={'in_f': art_in},
                 with_param = argo_range(len(os.listdir(Path('in_dir')))),
                 key='get-e-{{item}}'
                 )
    step2 = Step(name='step2',
                 template=PythonOPTemplate(getDF, image='python_diy:3.8'),
                 parameters={'in_es': step1.outputs.parameters['e'],
                             'in_names': step1.outputs.parameters['name'],
                             'in_paths': step1.outputs.parameters['xyz_path']})
    wf.add(step1)
    wf.add(step2)
    wf.submit()
    while wf.query_status() in ["Pending", "Running"]:
        time.sleep(1)

    assert(wf.query_status() == "Succeeded")
    step = wf.query_step(name="step2")[0]
    assert(step.phase == "Succeeded")
    download_artifact(artifact=step.outputs.artifacts['out_df'])


if __name__ == '__main__':
    test_dflow_v3()


完整代码如上,需要注意的是:
os.path.abspath() 是返回一个str,而云端(linux)上的path和local(win)下面的path不一样
所以在拼info的时候要用str,而不是path

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值