将代码生成器带入TVM

为了使数据科学家不必担心开发新模型时的性能,硬件后端提供程序(例如Intel,NVIDIA,ARM等)可以提供诸如cuBLAS或cuDNN之类的内核库以及许多常用的深度学习内核,或者提供诸如此类的框架。例如带有图形引擎的DNNL或TensorRT,使用户以某种方式描述其模型以实现高性能。此外,新兴的深度学习加速器还具有自己的编译器,内核库或运行时runtime框架。

当用户尝试在新的内核库或设备上工作时,必须学习新的编程接口。结果,对统一编程接口的需求变得越来越重要,使所有用户和硬件后端提供程序都站在同一页面上。

为了与广泛使用的深度学习框架共享编程接口,许多硬件设备提供商,尝试将其设备后端集成到TensorFlow。由于TensorFlow没有为新的后端提供正式的后端接口,必须破解TensorFlow进行注册,这需要对许多源文件进行更改,使将来的维护变得困难。

演示了作为硬件后端提供的程序,如何轻松利用自带代码生成(BYOC)框架,将硬件设备的内核库/编译器/框架集成到TVM。利用BYOC框架的最重要优点,设备的所有相关源文件都是独立的,设备的代码源/运行时可插入TVM代码库。这意味着1)使用代码源的TVM代码库将在上游兼容,以及2)TVM用户可以根据需要选择启用代码源/运行时。

的其余部分,首先说明可能需要带有BYOC的TVM的情况,然后概述BYOC编译和运行时流程。然后,分步说明如何使用英特尔DNNL(又名MKL-DNN,OneDNN)作为运行示例,将供应商库或执行引擎与BYOC集成到TVM。

将ASIC加速器带入TVM

首先,让做一个场景,说明为什么要将加速器引入TVM,以及BYOC框架可以包括哪些功能。

刚刚构建了一个具有ARM CPU和出色的加速器的边缘设备平台,该平台为常见的图像分类模型提供了出色的性能。加速器在Conv2D,ReLU,GEMM和其它广泛使用的CNN算子上表现良好。

不幸的是,对象检测模型也越来越受欢迎,并且客户需要在平台上同时运行图像分类和对象检测模型。尽管加速器能够执行对象检测模型中的几乎所有算子,但缺少一个算子(例如,非最大抑制,NMS)。

让TVM执行不受支持的算子

由于TVM具有用于不同后端的多个代码源,开源社区很容易在短时间内在CPU或GPU上实现新的算子。理想情况下,如果将加速器的编译流程与BYOC集成到TVM,则TVM将执行中继图分区,以将部分图卸载到加速器,同时将其它图保持在TVM上。表明平台能够运行所有模型,而不必担心新的运营商。

自定义图形级优化

ASIC加速器必须具有自己的编译流程。可能是以下情况之一:

生成图形表示并将其提供给图形引擎:可能拥有自己的图形引擎,该引擎能够在加速器上执行图形(或神经网络模型)。例如,英特尔DNNL和NVIDIA TensorRT都使用引擎来运行整个图形或模型,能够1)减少算子之间的内存事务,以及2)通过算子融合优化图形执行。

为了实现以上两个优化,可能需要在编译期间处理图形。例如,Conv2D和偏差加法是TVM中的两个单独的算子,可能是加速器上的一个算子(具有偏差加法功能的Conv2D)。在这种情况下,可能需要通过将conv2d - add图形模式替换为your_conv2d_with_bias节点来优化图形。

如果编译流程属于这种情况,建议阅读中的所有其余部分,但跳过将DNNL带到TVM:C源代码生成

生成汇编代码并将其编译为可执行的二进制文件:如果没有像前面那样的平台的端到端执行框架,则可能有编译器以ISA的汇编代码编译程序。为了将汇编代码提供给编译器,将需要一个代码生成器,从Relay图生成和优化汇编代码。

如果编译流程属于这种情况,建议阅读中的所有其余部分,但跳过将DNNL引入TVM:JSON Codegen / Runtime

BYOC的工作方式

简要解释BYOC框架是如何工作的。有关底层框架组件及其实现的更多详细说明,请参考开发者文档。总之,给定图1中的中继图,BYOC框架执行以下步骤:

图1:原始中继图。

1.图注解

制作用户提供的中继图,第一步是在图中注释可能卸载到加速器的节点。将需要遵循“将DNNL引入TVM:注释规则”以实现受支持的算子的白名单,或定制组合算子的图形模式列表。示例注释结果如图2所示。

图2:带注解的图。

2.图变换

第二步是基于注释对图形进行转换和优化。具体来说,BYOC执行以下转换。

2.1:合并编译器区域:如图2所示,图中现在有许多“区域”可以卸载到加速器中,实际上其中一些区域可以合并,以减少数据传输和内核启动开销。因此,步骤2.1使用贪婪算法来合并尽可能多的那些区域,同时保证功能正确性。结果如图3所示。

图3:合并编译器区域后。

2.2:分区图:对于上一步中的每个区域,创建一个带有属性的Relay函数,Compiler以指示该Relay函数应该完全卸载到加速器上,如图4所示。

图4:图分区之后。

3.代码生成

现在知道应该卸载中继图的哪一部分了。在此步骤中,将每个中继功能依次发送Compiler=your_accelerator到代码源。代码生成器应将Relay函数编译为,与自己的编译流程相匹配的形式。可以是C源代码或任何文本格式。

最后,所有已编译的函数将与其它未卸载的Relay函数一起.so由TVM export_libraryPython API序列化为单个文件。换句话说,.so运行此流程后,用户将仅获得一个文件。

4.运行时runtime

可能还需要实现运行时,初始化图形引擎(如果适用)并执行已编译的函数。在推理期间,当TVM运行时遇到图4中的相应函数调用时,TVM运行时(即图形运行时或VM)将利用运行时来调用已卸载的函数。运行时负责使用给定的输入张量,启动编译后的函数。将数组结果填充到输出张量数组中。

以DNNL为例,演示如何使用BYOC框架实现上述工作流程。所有引用的代码和行号均基于TVM存储库的master分支commit 8a0249c

将DNNL带到TVM:注释规则

BYOC框架为提供了两种描述受支持的算子和模式的方法。可以同时使用。以DNNL为例来说明如何使用。将代码源的注释规则放在下python/tvm/relay/op/contrib/your_codegen_name.py。

单一运营商规则

可使用BYOC API直观地指定加速器支持哪些中继算子。例如,使用以下代码段,构建一条规则,该规则表明DNNL代码源支持Conv2D:

@tvm.ir.register_op_attr("nn.conv2d", "target.dnnl")

def _dnnl_conv2d_wrapper(attrs, args):

return True

target.dnnl将向中继nn.conv2d算子注册一个新属性。通过这种方式,BYOC注释可以target.dnnl()为图中的每个算子调用,检查DNNL代码源中是否支持。

另一方面,为每个操作员编写上面的代码段可能很繁琐。对于DNNL实施,实现了一个辅助函数_register_external_op_helper,使用更方便:

def _register_external_op_helper(op_name, supported=True):

@tvm.ir.register_op_attr(op_name, "target.dnnl")

def _func_wrapper(attrs, args):

return supported

return _func_wrapper

_register_external_op_helper("nn.batch_norm")

_register_external_op_helper("nn.conv2d")

_register_external_op_helper("nn.dense")

_register_external_op_helper("nn.relu")

_register_external_op_helper("add")

_register_external_op_helper("subtract")

_register_external_op_helper("multiply")

在上面的示例中,指定了DNNL代码源可以支持的算子列表。

图形模式规则

加速器或编译器可能已将某些模式(例如Conv2D + add + ReLU)优化为单个指令或API。在这种情况下,可以指定从图形模式到指令/ API的映射。对于DNNL,Conv2D API已经包含了偏差加法,允许附加下一个ReLU,将DNNL称为以下代码片段:

DNNLConv2d(const bool has_bias = false, const bool has_relu = false) {

// ... skip ...

auto conv_desc = dnnl::convolution_forward::desc(

dnnl::prop_kind::forward_inference,

dnnl::algorithm::convolution_direct,

conv_src_md, conv_weights_md, conv_bias_md, conv_dst_md,

strides_dims, padding_dims_l, padding_dims_r);

// Attach ReLU

dnnl::primitive_attr attr;

if (has_relu) {

dnnl::post_ops ops;

ops.append_eltwise(1.f, dnnl::algorithm::eltwise_relu, 0.f, 0.f);

attr.set_post_ops(ops);

}

auto conv2d_prim_desc = dnnl::convolution_forward::primitive_desc(

conv_desc, attr, engine_);

// ... skip ...

在这种情况下,除了用于单个conv2d,映射图模式conv2d+relu到DNNLConv2d(false, true),映射conv2d+add+relu到DNNLConv2d(true, true)。可使用以下代码片段实现目的:

def make_pattern(with_bias=True):

data = wildcard()

weight = wildcard()

bias = wildcard()

conv = is_op('nn.conv2d')(data, weight)

if with_bias:

conv_out = is_op('add')(conv, bias)

else:

conv_out = conv

return is_op('nn.relu')(conv_out)

@register_pattern_table("dnnl")

def pattern_table():

conv2d_bias_relu_pat = ("dnnl.conv2d_bias_relu", make_pattern(with_bias=True))

conv2d_relu_pat = ("dnnl.conv2d_relu", make_pattern(with_bias=False))

dnnl_patterns = [conv2d_bias_relu_pat, conv2d_relu_pat]

return dnnl_patterns

在DNNL示例中,实现了两个具有不同名称的模式,以便可以在代码源中轻松识别。这些模式以中继模式语言实现。

使用模式表,使用Relay传递来执行

%1 = nn.conv2d(%data, %weight, ...)

%2 = add(%1, %bias)

%3 = nn.relu(%2)

%1 = fn(%input1, %input2, %input3,

Composite="dnnl.conv2d_bias_relu",

PartitionedFromPattern="nn.conv2d_add_nn.relu_") {

%1 = nn.conv2d(%input1, %input2, ...)

%2 = add(%1, %input3)

nn.relu(%2)

}

%2 = %1(%data, %weight, %bias)

DNNL代码生成器,获取模式名称conv2d_bias_relu,映射%1到DNNLConv2d(true, true)。

可能已经注意到,复合函数中还有一个名为“ PartitionedFromPattern”的属性。如果模式包含wildcard算子,这可能会有所帮助。例如,可能有一个模式表("conv2d_with_something", conv2d -> *):

def make_pattern(with_bias=True):

data = wildcard()

weight = wildcard()

conv = is_op('nn.conv2d')(data, weight)

return wildcard()(conv)

在这种情况下,将获得带有的复合函数Composite=conv2d_with_something,不知道实际匹配的图形。那就是PartitionedFromPattern起作用的地方。通过查看匹配图是否为conv2d -> add或conv2d -> relu,可以知道是否PartitionedFromPattern为nn.conv2d_add_或nn.conv2d_nn.relu_。

将DNNL引入TVM:中继图转换

利用上一步中的注释规则,现在可以应用BYOC中继传递列表,将中继图从图1转换为图4:

mod = create_relay_module_from_model() # Output: Figure 1

mod = transform.MergeComposite(pattern_table)(mod)

mod = transform.AnnotateTarget(["dnnl"])(mod) # Output: Figure 2

mod = transform.MergeCompilerRegions()(mod) # Output: Figure 3

mod = transform.PartitionGraph()(mod) # Output: Figure 4

每个中继传递都可以映射到在BYOC工作原理中引入的步骤。

将DNNL引入TVM:JSON代码生成/运行时

让实现将中继图序列化为JSON表示的DNNL代码源,然后实现DNNL JSON运行时以反序列化并执行该图。如果尝试实现一个代码生成器来生成C兼容程序,需要直接进入下一部分。

为了使DNNL JSON的代码生成/运行在TVM就这个例子中工作,确保DNNL可以在机器上,建立TVMset(USE_DNNL_CODEGEN ON)中config.cmake。

DNNL代码生成是在中实现的src/relay/backend/contrib/dnnl/codegen.cc。以两种形式实现了DNNLUSE_JSON_RUNTIME代码生成,在跟踪代码时,可以专注于宏所覆盖的部分。

首先使用TVM注册API(L510)注册代码源。该注册使TVM编译引擎以Compiler=<your codegen> 向调度Relay功能relay.ext.<your codegen>。然后,实现DNNL编译器(L490)的入口函数。阅读代码段中嵌入的注释以获取详细信息:

runtime::Module DNNLCompiler(const ObjectRef& ref) {

// "ref" should be the paritioned Relay function with kCompiler=dnnl.

CHECK(ref->IsInstance<FunctionNode>());

auto func = Downcast<Function>(ref);

// Get the function name as the symbol to match in runtime.

auto func_name = GetExtSymbol(func);

// Serialize the function to a JSON string (introduce later).

DNNLJSONSerializer serializer(func_name, func);

serializer.serialize();

std::string graph_json = serializer.GetJSON();

// The constant tensor names that have been bound to the module.

// All constant tensors will be serialzied along with the JSON graph

// when export_library is invoked.

auto params = serializer.GetParams();

// The function to create DNNL JSON runtime (introduce later).

const auto* pf = runtime::Registry::Get("runtime.DNNLJSONRuntimeCreate");

CHECK(pf != nullptr) << "Cannot find JSON runtime module to create";

// Create a DNNL runtime module that can run the serialized function.

auto mod = (*pf)(func_name, graph_json, params);

return mod;

}

TVM_REGISTER_GLOBAL("relay.ext.dnnl").set_body_typed(DNNLCompiler);

每个运行时模块仅负责一个中继功能,这意味着可能在单个.so文件中包含多个DNNL运行时模块。

DNNL JSON序列化

接下来,实现DNNL JSON序列化器(L429)。从BYOC JSON代码生成器(src / relay / backend / contrib / codegen_json / codegen_json.h)派生。DNNL JSON序列化程序中的特殊过程,将组合函数调用序列化为DNNL JSON运行时可以解释的JSON节点。假设有一个与pattern匹配的复合函数dnnl.conv2d_relu, BYOC JSON代码生成器将生成以下JSON节点:

{

op: "kernel",

name: "dnnl.conv2d_relu",

inputs: [[0, 0, 0], [1, 0, 0]],

attrs: {

PartitionedFromPattern: ["nn.conv2d_nn.relu_"],

shape: [1, 32, 14, 14]

}

}

问题在于,在运行时仍然需要Conv2D属性,例如padding和stride,但是BYOC JSON序列化器仅附加复合函数的属性,而不附加主体算子。另一方面,定制的DNNL JSON序列化程序将第一个也是唯一的Conv2D的属性附加到复合函数中,以生成以下JSON节点:

{

op: "kernel",

name: "dnnl.conv2d_relu",

inputs: [[0, 0, 0], [1, 0, 0]],

attrs: {

shape: [1, 32, 14, 14],

data_layout: ["NCHW"],

kernel_layout: ["OIHW"],

strides: [1, 1],

padding: [1, 1, 1, 1]

}

}

从DNNL JSON序列化器可以看出,可以自定义序列化器以生成JSON中的任何形式,只要JSON运行时可以解释即可。

DNNL JSON运行时

然后,实现DNNL JSON运行时以解释和执行序列化的JSON图。放在下面src/runtime/contrib/dnnl/dnnl_json_runtime.cc

同样,首先注册两个API来创建运行时,以便可以在任何地方使用。在runtime.DNNLJSONRuntimeCreate被序列化后的上一部分中使用,并且runtime.module.loadbinary_dnnl_json装载时也可使用.so了。

// Create a DNNL JSON runtime to interpret and execute the given JSON graph.

runtime::Module DNNLJSONRuntimeCreate(String symbol_name, String graph_json,

const Array<String>& const_names) {

auto n = make_object<DNNLJSONRuntime>(symbol_name, graph_json, const_names);

return runtime::Module(n);

}

TVM_REGISTER_GLOBAL("runtime.DNNLJSONRuntimeCreate")

.set_body_typed(DNNLJSONRuntimeCreate);

TVM_REGISTER_GLOBAL("runtime.module.loadbinary_dnnl_json")

.set_body_typed(JSONRuntimeBase::LoadFromBinary<DNNLJSONRuntime>);

现在,解释DNNL JSON运行时实现。基本的类结构为:

class DNNLJSONRuntime : public JSONRuntimeBase {

const  char* type_key() const { return  "dnnl_json"; }

void Init(const Array<NDArray>& consts) override {

// Initialize the DNNL graph engine.

BuildEngine();

// Setup constants entries for weights.

CHECK_EQ(consts.size(), const_idx_.size())

<< "The number of input constants must match the number of required.";

SetupConstants(consts);

}

void Run() override {

// 1. Fill in the input buffers.

// 2. Invoke the engine through intepreting the stream.

// 3. Read and fill output buffers.

}

}

该Init功能是负责通过解释JSON图形字符串建设DNNL引擎(见L93的BuildEngine),并填补了固定的权重,以相应的数据输入缓冲区(SetupConstant在JSON运行基类来实现,所以需要调用它在Init)。即使运行了多次推断,该函数也只会被调用一次。

接下来,Run函数(L64)首先将输入张量(可能来自用户输入或恒定权重)写入在构建DNNL引擎时初始化的相应DNNL存储缓冲区。然后启动DNNL引擎以执行JSON图。最后,将DNNL输出存储缓冲区写回到相应的输出张量。

由于DNNL JSON运行时中的其余实现都是DNNL特有的,不做详细介绍。尽管DNNL JSON运行时是一个很好的开始,但JSON运行时可以完全自定义以满足要求。

将DNNL带到TVM:C源代码生成

实现DNNL代码生成器,该代码生成器生成C源代码,该源代码调用DNNL API来执行中继图。如果尝试实现一个代码生成器,生成其它图形表示形式(如JSON格式)。

为了能够在TVM CODEGEN对这个例子的工作DNNL C源代码,确保DNNL可以在机器上,建立TVMset(USE_DNNL_CODEGEN C_SRC)中config.cmake。

DNNL代码生成是在中实现的src/relay/backend/contrib/dnnl/codegen.cc。由于在这个文件用于说明目的实现的代码生成DNNL两种形式,可以专注于部分被覆盖USE_JSON_RUNTIME宏跟踪代码时。

首先使用TVM注册API(L510)注册代码源。该注册使TVM编译引擎以Compiler=<your codegen> 向调度Relay功能relay.ext.<your codegen>。实现DNNL编译器的入口函数(L490):

runtime::Module DNNLCompiler(const ObjectRef& ref) {

DNNLModuleCodegen dnnl;

return dnnl.CreateCSourceModule(ref);

}

TVM_REGISTER_GLOBAL("relay.ext.dnnl").set_body_typed(DNNLCompiler);

每个运行时模块仅负责一个中继功能,这意味着可能在单个.so文件中包含多个DNNL运行时模块。

然后,在L362中派生CSourceModuleCodegenBase实施。而负责其它模块级过程,如序列化的,只需要实现DNNL代码生成函数(L389):DNNLModuleCodegenCSourceModuleCodegenBaseCreateCSourceModule

runtime::Module CreateCSourceModule(const ObjectRef& ref) override {

// Include headers

// ...skip...

code_stream_ << "#include <dnnl/dnnl_kernel.h>\n";

// ...skip...

// "ref" should be the paritioned Relay function with kCompiler=dnnl.

CHECK(ref->IsInstance<FunctionNode>());

auto res = GenDNNLFunc(Downcast<Function>(ref));

// "code" is the generated C code with DNNL APIs.

std::string code = code_stream_.str();

// "res" is a tuple of constant weights (symbols, values).

// All constant tensors will be serialzied along with the generated C code

// when export_library is invoked.

String sym = std::get<0>(res);

Array<String> variables = std::get<1>(res);

// Create a CSource module with all above artifacts.

const auto* pf = runtime::Registry::Get("runtime.CSourceModuleCreate");

CHECK(pf != nullptr) << "Cannot find csource module to create the external runtime module";

return (*pf)(code, "c", sym, variables);

}

接下来,实现GenDNNLFunc(L365)来使用DNNL API生成可编译的C代码,如下所示。参阅嵌入的注释,以获取与TVM C源运行时模块兼容的功能接口的说明。

// The example Relay graph: conv2d -> add -> relu.

#include <cstdint>

#include <cstdlib>

#include <cstring>

#include <vector>

#include <tvm/runtime/c_runtime_api.h>

#include <tvm/runtime/container.h>

#include <tvm/runtime/packed_func.h>

#include <dlpack/dlpack.h>

#include <dnnl/dnnl_kernel.h>

using namespace tvm::runtime;

using namespace tvm::runtime::contrib;

// Execute the conv2d->add->relu graph with DNNL.

extern "C" void dnnl_0_(float* dnnl_0_i0, float* dnnl_0_i1,

float* dnnl_0_i2, float* out0) {

// Allocate intermediate buffers.

float* buf_0 = (float*)std::malloc(4 * 4608);

float* buf_1 = (float*)std::malloc(4 * 4608);

float* buf_2 = (float*)std::malloc(4 * 4608);

// Pre-implemented op-based DNNL functions.

dnnl_conv2d(dnnl_0_i0, dnnl_0_i1, buf_0, 1, 32, 14, 14, 32, 1, 0, 0, 3, 3, 1, 1);

dnnl_add(buf_0, dnnl_0_i2, buf_1, 1, 32, 12, 12);

dnnl_relu(buf_1, buf_2, 1, 32, 12, 12);

// Copy the final output to the corresponding buffer.

std::memcpy(out0, buf_2, 4 * 4608);

std::free(buf_0);

std::free(buf_1);

std::free(buf_2);

}

// The wrapper function with all arguments in DLTensor type.

extern "C" int dnnl_0_wrapper_(DLTensor* arg0,

DLTensor* arg1,

DLTensor* arg2,

DLTensor* out0) {

// Cast all DLTensor to primitive type buffers and invoke the above

// execution function.

dnnl_0_(static_cast<float*>(arg0->data),

static_cast<float*>(arg1->data),

static_cast<float*>(arg2->data),

static_cast<float*>(out0->data));

return 0;

}

// The TVM macro to generate TVM runtime compatible function "dnnl_0"

// from our generated "dnnl_0_wrapper_".

TVM_DLL_EXPORT_TYPED_FUNC(dnnl_0, dnnl_0_wrapper_);

预先实现的基于op的DNNL函数位于src / runtime / contrib / dnnl / dnnl.cc中

其余实现src/relay/backend/contrib/dnnl/codegen.cc都过于DNNL,无法进行详细介绍。主要思想是实现一个中继图访问者(L138)以访问给定的Relay函数,生成上面的C代码。只要代码生成器能够生成与TVM运行时兼容的C代码,就可以完全自定义代码生成器以符合要求。

C源代码编译

输出的DNNLCompiler是一个带有生成的C代码的文本格式的模块,该模块尚未被编译gcc为可执行二进制文件。实际上,生成的C代码将在用户调用时进行编译export_libray(mod),如以下代码片段所示:

def update_lib(lib):

# Include the path of src/runtime/contrib/dnnl/dnnl.cc

test_dir = os.path.dirname(os.path.realpath(os.path.expanduser(__file__)))

source_dir = os.path.join(test_dir, "..", "..", "..")

contrib_path = os.path.join(source_dir, "src", "runtime", "contrib")

# Setup the gcc flag to compile DNNL code.

kwargs = {}

kwargs["options"] = ["-O2", "-std=c++14", "-I" + contrib_path]

tmp_path = util.tempdir()

lib_name = 'lib.so'

lib_path = tmp_path.relpath(lib_name)

# The generated C code with DNNL APIs is compiled to a binary lib.so.

lib.export_library(lib_path, fcompile=False, **kwargs)

# Load the lib.so back to a runtime module.

lib = runtime.load_module(lib_path)

return lib

with tvm.transform.PassContext(opt_level=3):

json, lib, param = relay.build(mod, target=target, params=params)

lib = update_lib(lib)

rt_mod = tvm.contrib.graph_runtime.create(json, lib, ctx)

将DNNL引入TVM:使用DNNL Codegen / Runtime构建TVM

最后,在构建TVM时创建cmake / modules / contrib / DNNL.cmake,包含DNNL代码源。DNNL代码生成器在同一cmake文件中具有两个实现,根据需要专注于其中之一。

在准备好cmake文件之后,用户可以set(USE_DNNL_CODEGEN ON)在其中指定build/config.cmake启用DNNL代码生成。


将代码生成器带入TVM的更多相关文章

  1. TVM代码生成codegen

    TVM代码生成codegen 硬件后端提供程序(例如Intel,NVIDIA,ARM等),提供诸如cuBLAS或cuDNN之类的内核库以及许多常用的深度学习内核,或者提供框架例,如带有图形引擎的DNN ...

  2. 端到端TVM编译器(上)

    端到端TVM编译器(上) 摘要 将机器学习引入到各种各样的硬件设备中.AI框架依赖于特定于供应商的算子库,针对窄范围的服务器级gpu进行优化.将工作负载部署到新平台,例如手机.嵌入式设备和加速器(例如 ...

  3. TVM设计与构架构建

    TVM设计与构架构建 本文档适用于希望了解TVM体系结构和/或在项目上进行积极开发的开发人员.该页面的组织如下: 实例编译流程Example Compilation Flow描述TVM把一个模型的高级 ...

  4. TVM 架构设计

    TVM 架构设计 本文面向希望了解TVM体系结构和/或积极参与项目开发的开发人员. 主要内容如下: 示例编译流程概述了TVM将模型的高级概念转换为可部署模块的步骤. 逻辑架构组件部分描述逻辑组件.针对 ...

  5. springmvc SSM shiro redis 后台框架 多数据源 代码生成器

    A集成代码生成器 [正反双向(单表.主表.明细表.树形表,开发利器)+快速构建表单 下载地址    ; freemaker模版技术 ,0个代码不用写,生成完整的一个模块,带页面.建表sql脚本,处理类 ...

  6. 分享一个与ABP配套使用的代码生成器源码

    点这里进入ABP系列文章总目录 分享一个与ABP配套使用的代码生成器源码 真对不起关注我博客的朋友, 因最近工作很忙, 很久没有更新博客了.以前答应把自用的代码生成器源码共享出来, 也一直没有时间整理 ...

  7. ABP配套代码生成器(ABP Code Generator)帮助文档,实现快速开发

    ABP代码生成器介绍 针对abp这个框架做了一个代码生成器,功能强大.分为两大功能点,一个是数据层,一个是视图层. 数据服务层:通过它,可以实现表设计.领域层初始化.多语言.automapper自动注 ...

  8. 基于AgileEAS.NET SOA 中间件领域模型数据器快速打造自己的代码生成器

    一.前言 AgileEAS.NET SOA 中间件平台是一款基于基于敏捷并行开发思想和Microsoft .Net构件(组件)开发技术而构建的一个快速开发应用平台.用于帮助中小型软件企业建立一条适合市 ...

  9. Razor Engine,实现代码生成器的又一件利器

    Razor Engine,之前仅仅是ASP.NET MVC的一种View引擎,目前已经完全成为一种可以独立使用的模版引擎,并且已经成为了CodePlex上一个开源的项目(http://razoreng ...

随机推荐

  1. nginx 完美解决tp3.2.3 404问题

    最近我把Apache给换成nginx,当我把tp项目搬过去运行的时候发现404 错误 ,原来是因为nginx不支持 pathinfo 模式,需要自己配置 下面我配置 在server配置里面 locat ...

  2. spring boot 项目从配置文件中读取maven 的pom.xml 文件标签的内容。

    需求: 将pom.xml 文件中的版本号读取到配置文件并打印到日志中. 第一步: 在pom.xml 中添加以下标签. 第二步: 将version 标签的值读取到配置文件中 这里使用 @@  而不是  ...

  3. POJ 1386 欧拉路的判定

    题意:       给你n个单词,问你有没有一种排列方式可以所有单词的首部是相邻单词的尾部. 思路:       这个题目还挺基础的,就是个欧拉的判定,首先对于每一个单词,我们把他抽象成边,每个单词两 ...

  4. 将Android源码导入eclipse中的方法以及编译Android源码指定模块

    本文博客地址:http://blog.csdn.net/qq1084283172/article/details/53365659 将android源码导入eclipse.androidstudio. ...

  5. PAT 乙级 -- 1011 -- A+B和C

    问题简述 给定区间[-231, 231]内的3个整数A.B和C,请判断A+B是否大于C. 输入格式: 输入第1行给出正整数T(<=10),是测试用例的个数.随后给出T组测试用例,每组占一行,顺序 ...

  6. 使用MindSpore的线性神经网络拟合非线性函数

    技术背景 在前面的几篇博客中,我们分别介绍了MindSpore的CPU版本在Docker下的安装与配置方案.MindSpore的线性函数拟合以及MindSpore后来新推出的GPU版本的Docker编 ...

  7. Ubuntu执行命令时,不sudo提示权限不足,sudo提示找不到该命令

    问题:Ubuntu执行命令时,不sudo提示权限不足,sudo提示找不到该命令 补充描述:尝试将命令所在路径添加到/etc/profile中(所有用户环境变量),结果sudo -i切换到root用户后 ...

  8. XCTF.MISC 新手篇

    目录 泪目 1.this_is_flag 2.pdf 3.如来十三掌 4.give_you_flag 5.坚持60s 6.gif 7.掀桌子 8.ext3 9.stegano 10.SimpleRAR ...

  9. linux跨文件复制粘贴

    跨文件是这样的: 复制a.txt的第20行至第30行到b.txt中vi a.txt:2010yy:e b.txtp

  10. 『动善时』JMeter基础 — 19、JMeter配置元件【随机变量】

    目录 1.随机变量介绍 2.随机变量界面详解 3.随机变量的使用 (1)测试计划内包含的元件 (2)线程组界面内容 (3)随机变量界面内容 (4)HTTP请求界面内容 (5)查看结果 1.随机变量介绍 ...