1. 布景介绍

在字节跳动,根据深度学习的运用遍地开花,工程师关注模型作用的一同也需求关注线上服务一致性和功用,早期这一般需求算法专家和工程专家分工合作并紧密配合来完结,这种模式存在比较高的 diff 排查验证等本钱。

随着 PyTorch/TensorFlow 结构的流行,深度学习模型练习和在线推理完结了一致,开发者仅需求关注具体算法逻辑,调用结构的 Python API 完结练习验证进程即可,之后模型能够很方便的序列化导出,并由一致的高功用 C++ 引擎完结推理作业。提升了开发者练习到布置的体验。

但是,完好的服务一般还存在很多的预处理/后处理等业务逻辑,这类逻辑一般是把各种输入经过加工处理转变为 Tensor,再输入到模型,之后模型的输出 Tensor 再加工成方针格局,一些典型的场景如下:

  • Bert

字节跳动模型大规模部署实战

  • Resnet

字节跳动模型大规模部署实战

字节跳动模型大规模部署实战

咱们的方针便是为以上端到端的进程,供给主动化且一致的练习、推理计划,减轻人工开发推理进程、对齐 diff 等一系列问题,完结大规模的一致布置计划。

2. 中心问题

PyTorch/TensorFlow 等结构相对已经处理了模型的练习/推理一致的问题,因而模型计算自身不存在训推一体的问题了(算子功用优化不在本次讨论范围)。

中心要处理的问题便是:预处理和后处理需求供给高功用训推一体的计划。

对于此类逻辑,TensorFlow 2.x 供给了 tf.function(还不完善),PyTorch 供给了 TorchScript,其无一例外都是选择了原生 Python 语法子集。但即便强壮如此,依然存在不行疏忽的问题:

  • 功用:此计划大多根据虚拟机完结,虚拟机计划灵活而且十分可控,但深度学习结构中的虚拟机大多一般功用不行优秀。补充阐明一下,结构早期都是为 Tensor 计算规划,数组计算每个算子本钱很高,虚拟机的派发和调度本钱能够疏忽。但是,移植到程序言语编程层面开支难以疏忽,代码写多了就会成为功用瓶颈。据测试,TorchScript 解释器功用只有 Python 的 1/5 左右,tf.function 功用更差一些。
  • 功用不全:事实上运用到实在场景中,咱们依然能够找出很多 tf.function/TorchScript 不支持的重要功用,比方:自定义的资源不能打包,只能序列化内置类型;字符串只能做 bytes 处理,中文等 unicode 会形成 diff;容器有必要同构,不支持自定义类型等等…

再者,还有很多非深度学习任务,比方在自然言语处理中依然有很多非深度学习的运用或许子任务,如序列标示,言语模型解码,树模型的人工特征结构等任务,这些一般具有更灵活的特征范式,但一同都没有完好完结端到端的训推一体计划,依然有很多的开发以及正确性校验作业。

为了处理上述问题,咱们开发了一套根据编译的预处理计划:MATXScript!

3. MATXScript

在深度学习算法开发中,开发者一般运用 Python 进行快速迭代和试验,一同运用 C++ 开发高功用的线上服务,其间正确性校验和服务开发都会成为较重负担!

MatxScript(github.com/bytedance/m… 是一个 Python 子言语的 AOT 编译器,能够主动化将 Python 翻译成 C++,并供给一键打包发布功用。运用 MATXScript 能够让开发者快速进行模型迭代的一同以较低本钱完结高功用服务的布置。

中心架构如下:

字节跳动模型大规模部署实战

  • 最底层是纯 C++/CUDA 的根底库,由高功用算子专家开发。
  • 在根底库之上,准守约好封装出来 Python 的 库,能够用在 training 进程中。
  • 需求 inferencing 时,利用 MATXScript 能够把 Python 代码,翻译成对等的 C++ 代码,编译成动态链接库,加上模型及其他依靠的资源,一同打包发布即可。

其间,编译器作用十分要害,其间心流程如下:

字节跳动模型大规模部署实战

经过以上流程,用户所编写的预处理代码,能够被编译成 Pipeline 中的一个 JitOp,为了把前后处理和模型联动,咱们还开发了 tracing 体系(接口规划上参阅了 PyTorch),架构如下:

字节跳动模型大规模部署实战

根据 MATXScript,咱们能够练习和推理运用同一套代码,大大降低了模型布置的本钱。一同,架构和算法得到了解耦,算法同学彻底运用 Python 作业即可,架构同学专心于编译器开发及 Runtime 优化,在字节跳动,此计划得到了大规模布置验证!

  1. 小试牛刀

此处以最简略的英文文本预处理为例,展现一下 MATXScript 如何运用。

方针:把一段英文文本转成 indexes

  1. 编写一个基本的查字典的逻辑
classText2Ids:
def__init__(self)->None:
self.table:Dict[str,int]={
"hello":0,
"world":1,
"[UNK]":2,
}
deflookup(self,word:str)->int:
returnself.table.get(word,2)

def__call__(self,words:List[str])->List[int]:
return[self.lookup(w)forwinwords]
  1. 编写 Pipeline
importmatx
classWorkFlow:
def__init__(self):
#此处会进行代码编译,Python代码主动编译封装为Callable对象
self.text2ids=matx.script(Text2Ids)()
defprocess(self,texts):
ids=self.text2ids(texts)
returnids
#test
handler=WorkFlow()
print(handler.process("helloworldunknown"))
#output:[0,1,2]
  1. Trace 导出到 磁盘
#dump
mod=matx.trace(handler.process,"helloworld")
print(mod.run({"texts":"helloworld"}))
mod.save('./my_dir')
#load
mod=matx.load('./my_dir',-1)
print(mod.run({"texts":"helloworld"}))
  1. C++ 加载
#include<string>
#include<vector>
#include<map>
#include<iostream>
#include<matxscript/pipeline/tx_session.h>
usingnamespace::matxscript::runtime;
intmain()
{
//testcase
std::unordered_map<std::string,RTValue>feed_dict;
feed_dict.emplace("texts",Unicode(U"helloworld"));
std::vector<std::pair<std::string,RTValue>>result;
constchar*module_path="./my_dir";
constchar*module_name="model.spec.json";
{
//-1meancpu
autosess=TXSession::Load(module_path,module_name,-1);
autoresult=sess->Run(feed_dict);
for(auto&r:result){
std::cout<<"key:"<<r.first<<",value:"<<r.second<<std::endl;
}
}
return0;
}

完好的代码见:github.com/bytedance/m…

小结:以上是一个十分简略的纯 Python 完结的预处理逻辑,且能被一段通用的 C++ 代码加载运转,下面咱们结合模型展现一个实际的多模态端到端事例!

5. 多模态事例

此处以图文多模态(Bert+Resnet)为例,模型运用 PyTorch 编写,展现练习和布置中实际的作业。

  1. 配置环境

    a. 配置 gcc/cuda 等根底设施(一般是运维同学已经搞定)

    b. 安装 MATXScript 及根据此开发的根底库(text、vision等)

  2. 编写模型代码

    a. 此处省略,咱们能够参阅论文或其他开源完结自行搞定

  3. 编写预处理代码

a. text

fromtypingimportList,Dict,Tuple
importlibcut
importmatx
classVocabulary:
...
defutf8_decoder(s:List[bytes]):
return[x.decode()forxins]
classTextNDArrayBuilder:
...
classTextPipeline:
def__init__(self,mode:str="eval"):
self.mode=mode
self.cut_engine=libcut.Cutter('/path/to/cut_models',...)
self.vocab=matx.script(Vocabulary)('/path/to/vocab.txt')
self.decoder=matx.script(utf8_decoder)
self.input_builder=matx.script(TextNDArrayBuilder)(self.vocab)
defprocess(self,text:List[bytes]):
#List[bytes]是对齐C++的vector<string>
text:List[str]=self.decoder(text)
words:List[List[str]]=self.cut_engine(text)
batch_ids:List[List[int]]=self.vocab(words)
input_ids,segment_ids,mask_ids=self.input_builder(batch_ids,32)
ifself.mode=="train":
returninput_ids.torch(),segment_ids.torch(),mask_ids.torch()
returninput_ids,segment_ids,mask_ids

b. vision

fromtypingimportList,Dict,Tuple
importmatx
frommatximportvision
classVisionPipeline:
def__init__(self,
device_id:int=0,
mode:str="eval",
image_size:int=224,):
self.is_training=mode=='train'
self.mode=mode
...

defprocess(self,image,):
ifself.is_training:
decode_nds=self.random_crop_decode(image)
flip_nds=self.random_flip(decode_nds)
resize_nds=self.resize(flip_nds)
transpose_nd=self.transpose_norm(resize_nds,vision.SYNC)
else:
decode_nds=self.decode(image)
resize_nds=self.resize(decode_nds)
crop_nds=self.center_crop(resize_nds)
transpose_nd=self.transpose_norm(crop_nds,vision.SYNC)
ifself.mode=="trace":
returntranspose_nd
returntranspose_nd.torch()
  1. 接入 DataLoader

    a. TextPipeline 能够当成一个正常的 Python Class 接入 Dataset 即可

    b. VisionPipeline 涉及到 GPU 预处理,更适合按 batch 进行处理,需求自己独自结构一个 DataLoader(这儿埋个点,之后会开源字节跳动内部根据多线程的 DataLoader)

  2. 加上模型代码,开始练习吧

  3. 导出端到端的 Inference Model

classMultimodalEvalPipeline:
def__init__(self):
self.text_pipe=TextPipeline(mode="eval",...)
self.vision_pipe=VisionPipeline(mode="eval",...)
self.torch_model=torch.jit.load('/path/to/multimodal.jit',map_location='cuda:0')
self.tx_model_op=matx.script(self.torch_model,device=0)
defeval(self,texts:List[bytes],images:List[bytes])->List[float]:
input_ids,segment_ids,mask_ids=self.text_pipe.process(texts)
images=self.vision_pipe.process(images)
scores=self.tx_model_op(input_ids,segment_ids,mask_ids,images)
returnscores
#examples
example_batch_size=8
text_examples=['hello,world'.encode()]*example_batch_size
withopen('/path/image.jpg','rb')asf:
image_example=f.read()
image_examples=[image_example]*example_batch_size
#pipelineinstance
pipe=MultimodalEvalPipeline(...)
mod=matx.trace(pipe.eval,text_examples,image_examples)
#test
print(mod.run({"texts":text_examples,"images":image_examples}))
#save
mod.save('/path/to/my_multimodal')

小结:经过以上步骤,咱们即可完结端到端的练习&发布作业,且整个进程是纯 Python 代码完结的,能够彻底由算法同学自己操控。当然,假如模型计算自身还有功用问题,也是能够在背后经过主动改图优化作业完结。

注:完好代码示例见 github.com/bytedance/m…

6. 一致Server

在上个章节,咱们得到了一个算法同学发布的模型包,本章节论说假如用一致的服务进行加载和运转。

完好的 Server 包括:IDL 协议、Batching 战略、进/线程调度和排布、模型推理…

这儿,咱们只讨论模型推理这块,其他的都是能够按约好开发即可。咱们以一个 main 函数来示例模型加载和运转的进程:

#include<string>
#include<vector>
#include<map>
#include<iostream>
#include<matxscript/pipeline/tx_session.h>
usingnamespace::matxscript::runtime;
intmain()
{
//testcase
std::unordered_map<std::string,RTValue>feed_dict;
feed_dict.emplace("texts",List({String("helloworld")}));
feed_dict.emplace("images",List({String("......")}));
std::vector<std::pair<std::string,RTValue>>result;
constchar*module_path="/path/to/my_multimodal";
constchar*module_name="model.spec.json";
{
//cuda:0
autosess=TXSession::Load(module_path,module_name,0);
autoresult=sess->Run(feed_dict);
for(auto&r:result){
std::cout<<"key:"<<r.first<<",value:"<<r.second<<std::endl;
}
}
return0;
}

以上代码便是最简略的一个 C++ 加载多模态模型的事例,对 Server 开发的同学来说,只需进行简略的抽象和约好,即可把上述代码改形成一个一致的 C++ 模型服务结构。

7. 更多信息

咱们是字节跳动-AML-机器学习体系团队,致力于为公司供给一致的高功用训推一体化结构,一同也会经过火山引擎机器学习平台服务于合作企业,火山引擎机器学习平台估计 2023 年起供给 MATX 的相关支持,包括预置镜像环境、常用场景的公开样例、企业接入和运用进程中的技能保证等,能够到达练习和推理场景低本钱加速和一体化的作用。欢迎在 www.volcengine.com/product/ml-… 具体了解咱们的产品。

更多信息,可拜访 Github:github.com/bytedance/m…