1.背景在字节跳动,基于深度学习的应用遍地开花。工程师在关注模型效果的同时,也需要关注线上服务的一致性和性能。在早期,这通常需要算法专家。由工程专家分工合作、密切合作完成。这种模式的diff检测、验证等成本相对较高。随着PyTorch/TensorFlow框架的流行,深度学习模型训练和在线推理已经统一。开发者只需要关注具体的算法逻辑,调用框架的PythonAPI即可完成训练和验证过程。之后就可以方便的对模型进行序列化和导出了,统一的高性能C++引擎完成推理工作。改进了从培训到部署的开发人员体验。但是一个完整的服务通常会有很多的前处理/后处理等业务逻辑。这类逻辑通常是将各种输入经过处理后转化为Tensor,然后输入到模型中,再将模型的输出Tensor处理成目标格式,一些典型场景如下:BertResnet的目标是为上述端到端流程提供自动化统一的训练和推理解决方案,缓解人工开发推理流程、alignmentdiff等一系列问题,以及实现大规模统一部署方案。2.核心问题PyTorch/TensorFlow等框架相对解决了统一模型训练/推理的问题,所以模型计算本身不存在训练推理一体化的问题(算子性能优化不在本次讨论范围内).需要解决的核心问题是:前处理和后处理需要为训练和推送提供一个高性能的一体化解决方案。对于这类逻辑,TensorFlow2.x提供了tf.function(并不完美),PyTorch提供了TorchScript,无一例外地选择了原生Python语法的子集。但即便如此强大,也存在着不容忽视的问题:性能:该方案多基于虚拟机实现。虚拟机方案灵活,可控性强,但深度学习框架中的大部分虚拟机通常不够好。补充一下,这个框架早期是为Tensor计算设计的。阵列计算中每个算子的成本非常高,虚拟机的分配和调度成本可以忽略不计。但是,移植到编程语言编程层面的开销是难以忽视的,写太多的代码会成为性能瓶颈。据测试,TorchScript解释器的性能只有Python的1/5左右,tf.function的性能更差。功能不全:其实应用到实际场景中,我们还是可以发现很多tf.function/TorchScript不支持的重要功能,比如:自定义资源无法打包,只能序列化内置类型;strings只能作为bytes处理,中文等unicode会造成diff;容器必须是同构的,不支持自定义类型等等。。。而且,还有很多非深度学习的任务,比如在自然语言处理或者子任务上还有很多非深度学习的应用,比如序列标注、语言模型解码、树模型人工特征构建等任务,这些通常具有更灵活的特征范式,但同时,还没有完全实现端到端的一体化训练和推理解决方案,还有仍然有很多发展和正确性。性别验证工作。为了解决以上问题,我们开发了一套基于编译的预处理方案:MATXScript!3.MATXScript在深度学习算法开发中,开发者通常使用Python进行快速迭代和实验,同时使用C++开发高性能在线服务,其中正确性验证和服务开发将成为沉重的负担!MatxScript(https://github.com/bytedance/matxscript)是一个针对Python子语言的AOT编译器,可以自动将Python翻译成C++,并提供一键打包发布功能。使用MATXScript允许开发人员快速迭代模型,同时以更低的成本部署高性能服务。核心架构如下:底??层是纯C++/CUDA基础库,由高性能算子专家开发。在基础库的基础上,按照协议封装了Python库,可以在训练过程中使用。当需要推理时,可以使用MATXScript将Python代码翻译成等效的C++代码,编译成动态链接库,添加模型和其他依赖资源,一起打包发布。其中,编译器的作用非常重要,其核心流程如下:通过上述流程,可以将用户编写的预处理代码编译成Pipeline中的JitOp。为了将预处理和模型联系起来,我们还开发了一个跟踪系统(界面设计参考PyTorch),架构如下:基于MATXScript,我们可以使用同一套代码进行训练和推理,大大降低了模型部署的成本。同时架构和算法解耦,算法同学完全可以使用Python来工作。建筑专业的学生专注于编译器开发和运行时优化。在字节跳动,这个方案已经得到了大规模部署的验证!4、这里小试一下,以最简单的英文文本预处理为例,展示如何使用MATXScript。目标:将一段英文文本转换成索引,写一个基本的字典查找逻辑classText2Ids:def__init__(self)->None:self.table:Dict[str,int]={"hello":0,"world":1,"[UNK]":2,}deflookup(self,word:str)返回self.table.get(word,2)def__call__(self,words:List[str])return[self.lookup(w)forwinwords]writePipelineimportmatxclassWorkFlow:def__init__(self):#这里会进行代码编译,Python代码会自动编译封装成一个Callable对象self.text2ids=matx.script(Text2Ids)()defprocess(self,texts):ids=self.text2ids(texts)returnids#testhandler=WorkFlow()print(handler.process("helloworldunknown"))#output:[0,1,2]跟踪导出到磁盘#dumpmod=matx.trace(handler.process,"helloworld")print(mod.run({"texts":"helloworld"}))mod.save('./my_dir')#loadmod=matx.load('./my_dir',-1)print(mod.run({"texts":"helloworld"}))C++load#include#include#include<地图>#include#includeusingnamespace::matxscript::runtime;intmain(){//测试用例std::unordered_mapfeed_dict;提要字典。emplace("文本",Unicode(U"你好世界"));std::vector>结果;constchar*module_path="./my_dir";constchar*module_name="model.spec.json";{//-1表示cpuautosess=TXSession::Load(module_path,module_name,-1);autoresult=sess->Run(feed_dict);for(auto&r:result){std::cout<<"key:"<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)如果self.mode=="train":返回input_ids.torch(),segment_ids.torch(),mask_ids.torch()返回input_ids,segment_ids,mask_idsb。visionfromtypingimportList,Dict,TupleimportmatxfrommatximportvisionclassVisionPipeline:def__init__(self,device_id:int=0,mode:str="eval",image_size:int=224,):self.is_training=mode=='火车'self.mode=mode...defprocess(self,image,):如果self.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_ndreturntranspose_nd.torch()已连接到DataLoadera。TextPipeline可以作为普通的PythonClassb连接到Dataset。VisionPipeline涉及GPU预处理,更适合批量处理。需要自己构建一个DataLoader(这里买点,然后开源字节跳动内部的多线程DataLoader)加上模型代码,开始训练吧。导出端到端推理模型类MultimodalEvalPipeline: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_locatinotallow='cuda:0')self.tx_model_op=matx.script(self.torch_model,device=0)defeval(self,文本:列表[字节],图像:列表[字节])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)返回分数#examplesexample=_batch_size_examples=['你好,世界'.encode()]*example_batch_sizewithopen('/path/image.jpg','rb')asf:image_example=f.read()image_examples=[image_example]*example_batch_size#pipelineinstancepipe=MultimodalEvalPipeline(...)mod=matx.trace(pipe.eval,text_examples,image_examples)#testprint(mod.run({"texts":text_examples,"images":image_examples}))#savemod.save('/path/to/my_multimodal')总结:经过以上步骤,我们就可以完成端到端的training&publishing工作,整个过程由纯Python代码完成,算法同学完全可以掌控。当然,如果模型计算本身仍然存在性能问题,也可以通过自动更改图像优化工作在幕后解决。注:完整代码示例见https://github.com/bytedance/matxscript/tree/main/examples/e2e_multi_modal6.统一服务器上一章我们拿到了一个算法同学发布的模型包。本章讨论如何使用统一服务加载和运行。一个完整的服务器包括:IDL协议、Batching策略、入口/线程的调度与安排、模型推理……这里只讨论模型推理,其他的都可以按照协议进行开发。我们用一个main函数来说明模型加载运行的过程:#include#include#include