PyTrac 1跟踪JIT和LiBrand C++ API集成PyTorch into NodeJS

今天,在Pythort开发者大会上,PyTorch团队宣布了PyTorch 1.0预览版的计划和发布,该预览版具有许多不错的功能,例如模型图的JIT(有跟踪和无跟踪)以及竖笛,PyTrac C++接口,其中一个最重要的发布公告在我看来是今天做的。

考虑到人们对理解这种新的API的工作方式非常感兴趣,我决定写这篇文章,展示了很多机会,在发布PyTrac C++ API之后,现在是开放的。在这篇文章中,我将使用NoDEJS C++附加组件将PyTrar推断集成到本地NoDEJS中,就像一个集成不同框架/语言的例子,现在可以使用c++ API。

以下是最终结果:

如你所见,集成是无缝的,我可以使用一个跟踪的resnet作为计算图模型,并向它提供任何张量来获得输出预测。

介绍

简单地说,libtorch是PyTorch的库版本。它包含了Py火炬使用的基础,如阿滕(张量图书馆)它包含所有张量运算和方法。libtorch还包含签名,它是自动微分ATen张量的元件。

对于那些刚刚开始学习的人,我要提醒你们注意的是要小心使用阿吞和欧特格拉德都能产生的张量,不要把它们混在一起,ATen将会返回普通张量(当你使用命名空间)而签名函数(来自火炬名称空间)将返回变量,增加了自动微分机构。

有关PyTorch内部工作原理的更广泛教程,请看一下我之前的教程PyTorch内部架构

libtorch可以从Pytorch网站它只在一段时间内作为预览版提供。您还可以在其中找到文档此网站,主要是Doxygen渲染的文档。我发现图书馆很稳定,这是有道理的,因为它实际上是暴露了俾火炬稳定的基础,然而,在开始使用类库组织时,您可能会发现一些头部问题和一些与类库组织相关的小问题(希望很快就能解决)。

对NodeJS来说,我会用本机抽象库(南),它是最推荐的库(实际上基本上是一个头文件库)来创建NoDEJS C++附加组件和CJEW JS,因为libtorch已经提供了cmake文件,使我们的构建过程更加容易。然而,这里的重点将放在C++代码上,而不是在构建过程中。

发展的流程,追踪,序列化和加载模型可以在左侧的图中看到。

它首先在PyTorch (Python域)中执行开发过程和跟踪,然后在c++域(在我们的例子中是NodeJS附加组件)上加载和推断。

包装的张量

在Nodejs中,要创建一个对象作为JavaScript世界的一级公民,你需要继承ObjectWrap类,负责封装C++组件。

的ifndef TENSOR_H # define TENSOR_H # include
          
           # include
           
            命名空间torchjs class tensor:public nan::objectwarp public:static nan_module_init(init);void set张量(at::张量张量){this-> m张量=张量;torch::tensor gettensor()返回这个->mtensor;静态V8::本地
            
             NeWistStand();私人:明确的张量();~张量();静态Nan_法(新);静态Nan_法(ToString);静态NaN::持久
             
              施工单位;private:torch::tensor mtensor;//命名空间torchjs endif
             
            
           
          

如你所见,大多数定义张量类的代码都是样板。这里的关键是张量将包装火炬:张量我们添加了两个特殊的公共方法(setTensorgetTensor)设置并获得内部焊炬张量。

我不会展示所有的实现细节,因为它的大部分是构建对象的NodeJS样板代码,等。我将重点介绍接触libtorch API的部分,在下面的代码中,我们创建一个小的张量的文本表示,显示在javascript()上。ToString公司方法):

Nan_方法(Tensor::ToString)Tensor*Obj=ObjectWrap::Unwrap
          
           (info.Holder ());std::字符串流ss;at::intlist sizes=obj->mtensor.sizes();ss<“tensor[type=”<<obj->mtensor.type()<<“,“;ss << "Size=" << Size << std::endl;info.getReturnValue().set(nan::new(ss.str()).toLocalChecked());
          

我们在上面的代码中所做的,只是从被包装的对象中得到内部张量对象吗展开它。在那之后,我们用张量大小(每个维度大小)及其类型(float,等等)。

包装张量创建操作

现在让我们为火炬::的函数,它负责创建由常量1填充的任何定义形状的张量。

nan_method(ones)//如果(info.length()<2)返回nan::throwerror(nan::new(“错误的参数数目”).toLocalChecked())参数的健全性检查;if (!info[0]->IsArray() || !info[1]->IsBoolean())返回Nan::ThrowError(Nan::New("Wrong argument types").ToLocalChecked());//获取参数(require_grad和张量形状)const bool require_grad = info[1]->BooleanValue();const v8::当地
          
           数组=信息[0].as
           
            ();const uint32_t length = array-> length ();//将v8::Array转换为std::vector std::vector
            
             调光;for(int i = 0;我
             
              v;int d = array->Get(i)->NumberValue();调光。向后推(d);} //调用libtorch,并创建一个新的火炬::张量对象//包装新的火炬::张量,它是由火炬::张量在::张量v =火炬::1 (dims,Torch::需要_Grad(需要_Grad));auto newinst=tensor::newInstance();张量*obj=nan::objectwarp::unwrap
              
               (纽因斯特);对象->设置传感器(V);info.GetReturnValue()这里(newinst);}
              
             
            
           
          

所以,让我们看一下这段代码。我们首先检查函数的参数。对于这个函数,我们期望张量形状有一个元组(一个javascript数组)和一个布尔值,指示我们是否要为这个张量节点计算渐变。在那之后,我们将V8 JavaScript类型的参数转换为本机C++类型。一旦我们有了所需的参数,然后我们打电话给火炬::的来自libtorch的函数,这个函数会产生一个新的张量,我们用a张量我们之前创建的用于包装它的类。

就是这样,我们只公开了一个可以用作本机JavaScript操作的火炬操作。

用于Pythort JIT的Intermezzo

引入的PyTorch JIT围绕着Torch脚本的概念。Torch脚本是Python语言的一个子集,它自带编译器和转换过程(优化,等等)。

可以通过两种不同的方式创建此脚本:使用跟踪JIT或提供脚本本身。在跟踪模式下,将访问您的计算图节点并记录操作以生成最终脚本,而脚本是一种模式,在这种模式下,考虑到Torch脚本的限制,您可以提供模型的描述。

注意,如果您的代码上有依赖于外部因素或数据的分支决策,跟踪不会像您预期的那样工作,因为它将记录图形的特定执行,因此提供脚本的另一个选项。然而,在大多数情况下,追踪是我们需要的。

为了理解差异,让我们来看看由跟踪和脚本生成的脚本模块中的中间表示(IR)。

@torch.jit.scriptdef happy_函数_script(x):ret=torch.rand(0)if true==true:ret=torch.rand(1)else:ret=torch.rand(2)return retdef happy_函数_trace(x):ret=torch.rand(0)if true==true:ret=torch.rand(1)else:ret=torch.rand(2)return rettracked_=torch.jit.trace(happy_f功能追踪(火炬张量(0),),检查_trace=false)

在上面的代码中,我们提供了两个函数,一个是使用@torch.jit.script装饰工,这是一种创建Torch脚本的脚本方法,当跟踪函数使用第二个函数时torch.jit.trace。并不是我故意在函数上添加了一个“True == True”的决定(它总是正确的)。

现在,如果我们检查这两种不同方法生成的IR,我们将清楚地看到跟踪和脚本方法之间的区别:

1)脚本逼近图(%x: Dynamic) {%16: int = prim::Constant[value=2]() %10: int = prim::Constant[value=1]() %7: int = prim::Constant[value=1]() %8: int = prim::Constant[value=1]() %9: int =:eq(%7,%8)%ret:dynamic=prim::if(%9)block0()%11:int[]=prim::listconstruct(%10)%12:int=prim::constant[value=6]()%13:int=prim::constant[value=0]()%14:int[]=prim::constant[value=[0,-1]()%ret.2:dynamic=aten::rand(%11,% 12,%13,%14)->(%ret.2)block1()%17:int[]=prim::listconstruct(%16)%18:int=prim::constant[value=6]()%19:int=prim::constant[value=0]()%20:int[]=prim::constant[value=[0,-1]()%ret.3:dynamic=aten::rand(%17,%18,%19,%20)->(%ret.3)返回(%ret);2)来自跟踪方法图(%0:long())%7:int=prim::constant[值=1]()%8:int[]=prim::listconstruct(%7)%9:int=prim::constant[值=6]()%10:int=prim::constant[值=0]()%11:int[]=prim::constant[值=0,-1]]() %12: Float(1) = aten::rand(%8,% 9,% 10,%11)返回(%12);

如我们所见,IR和。非常相似bepaly亚洲LLVM IR,注意,在跟踪方法中,记录的跟踪只包含来自代码的一条路径,真理的道路,而在脚本中,我们有两种分支选择。然而,即使在脚本,始终为false的分支可以通过死代码消除转换通道进行优化和删除。

pytorch-jit有很多用于循环展开的转换过程,死代码消除,等。你可以找到这些从这里经过。不是说像ONNX这样的其他格式的转换可以在这个中间表示(IR)的基础上实现,这很方便。

跟踪Resnet

现在,在NodeJS中实现脚本模块之前,让我们首先使用PyTorch(仅使用Python)跟踪ResNet网络:

traced_net = torch.jit.trace (torchvision.models.resnet18 (),torch.rand(1,3.224年,224))跟踪的_net.save(“resnet18_trace.pt”)

从上面的代码中可以看到,我们只需要提供一个张量的例子(在这种情况下,一批与3通道和单个图像大小224×224。之后,我们将跟踪的网络保存到一个名为resnet18_跟踪.pt

现在我们准备在nodejs中实现脚本模块,以便加载跟踪的文件。

包装脚本模块

这是nodejs中脚本模块的实现:

// JavaScript对象creationNAN_METHOD(ScriptModule::New) {if (info. isconstructcall()){//获取文件名参数v8:: string::Utf8Value param_filename(info[0]->ToString());const std::string filename=std::string(*param_filename);//使用该文件名script module*obj=new script module(文件名)创建新的脚本模块;obj - >包装(info.This ());info.GetReturnValue()这里(info.This ());其他V8::本地
          
           cons=nan::new(构造函数);info.getReturnValue().set(nan::newInstance(cons.toLocalChecked());}}
          

从上面的代码中可以看到,我们只是创建一个类来调用焊炬::jit::加载函数传递跟踪网络的文件名。我们还有JavaScript对象的实现,在这里,我们将参数转换为C++类型,然后创建一个新的torchjs::脚本模块

向前传球的包装也很简单:

nan_方法(script module::forward)script module*script_module=objectwarp::unwrap
          
           (info.Holder ());南:五月本地
           
            也许=南::
            
             (信息[0]);张量*张量=Nan::ObjectWrap::Unwrap
             
              (可能是.toLocalChecked());torch::张量torch_张量=张量-> get张量();torch::tensor output=script_module->mmodule->forward(torch_tensor).totensor();auto newinst=tensor::newInstance();张量*obj=nan::objectwarp::unwrap
              
               (纽因斯特);obj->settensor(输出);info.GetReturnValue()这里(newinst);}
              
             
            
           
          

如你所见,在这段代码中,我们只是收到一个张量作为论点,我们得到内部火炬:张量然后从脚本模块调用Forward方法,我们将输出包装在新的张量然后把它还给我。

就是这样,我们准备使用本地NodeJS构建的模块,如下例所示:

var torchjs=require(“/build/release/torchjs”);var script_module=new torchjs.script module(“resnet18_trace.pt”);var data=torchjs.ones([1,3.224年,224,假);var输出=script_module.forward(数据);

希望你喜欢!Libtorch为PyTorch在许多不同语言和框架中的紧密集成打开了大门,这是非常令人兴奋的,也是朝着生产部署代码方向迈出的巨大一步。

—克里斯汀S.佩隆

基督教的年代。佩隆

6评论

  1. bepaly亚洲非常酷的后基督徒!您在头文件和代码组织方面遇到了哪些问题?我设置了那个页面,所以我可以让它更整洁。

  2. 感谢你提供的信息。从跟踪和脚本模式获取IR的命令是什么?

    (还有一些地方写着“not that”,我想你的意思是“注意那个”)

留下评论

您的电子邮件地址将不会发布。

此网站使用Akismet来减少垃圾邮件。了解如何处理注释数据