TVM基础编程示例分析
一.TVM编程基础示例
前言
继前图灵奖获得者Hennessy和Patterson在ISCA 2018提出“A New Golden Age for Computer Architecture”,编译器界大神Chris Lattner在ASPLOS 2021提出了“The Golden Age of Compiler Design”。另一方面,2020年图灵奖授予了编译器“龙书”作者Jeffrey Ullman和Alfred Aho。编译器技术在新的时代背景下似乎又再次焕发了新的活力,成为了业界的热点。
作为现在最热门的AI计算场景,与编译器技术的结合自然成为了大家不约而同的技术路线。机器学习跨入深度学习时代后,比较老一代的计算框架基本将神经网络建模为计算图,其中算子为节点,张量为边。然后以拓扑序执行,辅以并行优化等。这种范式下,为了达到好的性能,一般需要对网络中的算子深度优化。但是,今天的神经网络结构日益复杂,算子种类也更加繁多。不同的算子参数、输入配置以及算子间的融合,使得需要优化的算子数量组合爆炸,一一硬刚不切实际,而且很多时候也缺乏专家经验和开发时间。为了挖掘极致的性能,同时使得新算子实现更为方便,基于编译技术的方法成为了主流。像TVM,XLA,Glow,nGraph,MindSpore,Jittor,MegEngine,ONNC,Tiramisu等等用到或是基于编译技术的计算框架层出不穷。
在这个方向上,TVM可以说是先驱者,一个端到端的深度学习编译器,在平台兼容性和性能等方面都有很好的表现,社区也非常活跃。但TVM代码读起来不太容易理解(编译器的代码好像都不太好读…)。TVM经过几年的快速演进,今天已是一个比较复杂的系统了,里边的功能很多。可以通过过一个最简单的例子来看看其大致处理流程。本文通过官方教程Working with Operators Using Tensor Expressions中的例程vecadd为例,介绍TVM的流程示例。

import tvm
import os

n = 1024

A = tvm.te.placeholder((n,), name=‘A’)
B = tvm.te.placeholder((n,), name=‘B’)
C = tvm.te.compute(A.shape, lambda i: A[i] + B[i], name=“C”)

s = tvm.te.create_schedule(C.op)

outer, inner = s[C].split(C.op.axis[0], factor=64)

s[C].parallel(outer)

tgt = tvm.target.Target(target=“llvm”, host=“llvm”)

fadd = tvm.build(s, [A, B, C], tgt, name=“vecadd”)

dev = tvm.device(tgt.kind.name, 0)
a = tvm.nd.array(np.random.uniform(size=n).astype(A.dtype), dev)
b = tvm.nd.array(np.random.uniform(size=n).astype(B.dtype), dev)
c = tvm.nd.array(np.zeros(n, dtype=C.dtype), dev)
fadd(a, b, c)
程序做的事就是两个向量的逐元素相加。这个case中不考虑复杂算子,不考虑Relay,不考虑复杂pass,不考虑复杂的schedule,不考虑auto-tuning机制,不考虑graph runtime等。也正是因为简单,分析处理流程可以抓住主干,避免陷入复杂的细节。麻省虽小,五脏俱全。包含了TVM主要流程中的几个关键要素。
整个过程会分量部分介绍。第一部分主要涉及计算定义与schedule的创建。TVM是基于Halide中algorithm与schedule分离的思想。简单而言,前者指定算什么,后者指定怎么算。下面两节就是分别对应计算的定义与schedule的构建。
定义计算
现实使用当中,多数情况下会通过前端的解析器,从已有的机器学习模型中导入。如from_onnx.py中的relay.frontend.from_onnx()函数,可以从onnx模型导入。但上面例子是单个算子的例子,直接通过TE(Tensor expression)定义的。
先来看下例子中的计算定义部分:
A = tvm.te.placeholder((n,), name=‘A’)
B = tvm.te.placeholder((n,), name=‘B’)
C = tvm.te.compute(A.shape, lambda i: A[i] + B[i], name=“C”)
通过TEDD,可构建可视化图如下:
在这里插入图片描述

上面语句中,首先通过placeholder()函数创建tensor对象。调用_ffi_api.Placeholder()函数,从Python调到C++层构建PlaceholderOpNode对象,然后输出tensor返回。主要流程如下:
te.placeholder() # operation.py
return ffi_api.Placeholder() # placeholder_op.cc
return placeholder()
return PlaceholderOp(…).output(0) # tensor.cc
n = make_object();

data
= std::move(n);
这里的返回类型,或者说上面的A,B类型为tvm.te.tensor.Tensor。C++层对应TensorNode类。TensorNode中关联的Operation对象,代表通过什么操作计算得到的。Operation的output()函数可以得到输出tensor。OperationNode的InputTensors()函数(纯虚函数,在各继承类中会实现,如ComputeOpNode::InputTensors())得到输入tensor。通过这样的方式在逻辑上形成计算图,表示了相互间的依赖关系。
接下去的compute()函数(实现在operation.py),主要用于根据给定用TE描述的计算,构建一个新的tensor。主要流程如下:

compute(shape, fcompute, …) # operation.py

dim_var = [tvm.tir.IterVar((0, s), x, 0) for x, s in zip(arg_names, shape[:out_ndim])] # expr.py
body = fcompute(*[v.var for v in dim_var])
body = convert(body)
op_node = _ffi_api.ComputeOp(name, tag, attrs, dim_var, body)
outputs = tuple(op_node.output(i) for i in range(num))
return outputs[0] if num == 1 else outputs
其中有几个关键步骤:

  1. 为每个axis创建tvm.tir.IterVar,对应循环变量。如上例中就只有一个axis,范围为[0,1024)。对应的C++层的IterVar类定义在var.h文件中。

  2. 语句body = fcompute(*[v.var for v in dim_var])最为关键,调用传入的lambda函数,返回的body类型为tvm.tir.expr.Add(继承关系:->BinaryOpExpr->PrimExprWithOp->ExprOp & PrimExpr)。lambda函数中的A[i]类型为TensorSlice(继承自ObjectGeneric与ExprOp),代表Tensor的切片。调用下面的函数前会使用TensorSlice::asobject()函数,转成ProducerLoad(expr.py和expr.h)对象,继承自PrimExpr。这里由于是加操作,因此会调用ExprOp的操作符重载函数__add__()。继而调用add()函数(定义在tir/generic.py)。该函数调用到C++层,相应的函数在tir/op/op.cc中,通过下面的宏注册:REGISTER_MAKE_BINARY_OP(_OpAdd, add);。实现如下:
    PrimExpr add(PrimExpr a, PrimExpr b, Span span) {
    BinaryOpMatchTypes(a, b, span);
    PrimExpr ret = arith::TryConstFoldtir::Add(a, b);
    if (ret.defined()) return ret;
    return tir::Add(a, b, span);
    }
    返回的是tir::Add对象,对应Python中的Add对象(定义在tir/expr.py)。
    调用convert()函数(实现在object_generic.py),对body对象进行转换,转化为TVM对象。经过转换后body类型为tvm.ir.container.Array。
    创建C++层的ComputeOp对象(实现在compute_op.cc)。这个对象中包含ComputeOpNode对象的引用。C++层中ComputeOp(继承自Operaton),对应Python中的对象类型为te.tensor.ComputeOp。Python层中ComputeOp(继承关系:ComputeOp->BaseComputeOp->Operation)。最后返回output张量对象,类型为te.tensor.Tensor。
    对于上面的例子,构建的数据结构大体如下:
    在这里插入图片描述

相关主要类简图:
在这里插入图片描述

图中也可以看到,Python与C++层中的对象有对应关系。这便于Python与C++间的调用,这也是TVM的特色之一。一般名为XXX的是相应XXXNode的引用(如ComputeOp与ComputeOpNode)。前者继承自ObjectRef,后者继承自Object。主要的内容是在XXXNode中,XXX中的->操作符重载了,将操作及访问会应用到XXXNode上。
Operation代表操作,如PlaceholderOp和ComputeOp。Tensor代表张量,TensorSlice表示Tensor的切片,如例子中A[i]。PrimExpr主要用于low-level的表示,是所有primitive expression的基类。Primitive expression处理POD数据类型。这里表示计算的Add和包含了张量的ProducerLoad都是PrimExpr。
稍微复杂些的常见例子是矩阵乘matmul:
k = tvm.te.reduce_axis((0, l), name=‘k’)
A = tvm.te.placeholder((n, l), name=‘A’)
B = tvm.te.placeholder((l, m), name=‘B’)
C = tvm.te.compute((n, m), lambda x, y: tvm.te.sum(A[x, k] * B[k, y], axis=k), name=‘C’)
与上例有所区别的是这里操作数都是二维的,且有reduce轴(计算过程中约减,因此输入中有,输出中没有的轴)。计算中使用了tvm.te.sum()(实现在python/tvm/tir/op.py)函数来reduce中间轴。函数的定义为:
sum = comm_reducer(lambda x, y: x + y, lambda t: const(0, dtype=t), name=“sum”) # tir/op.py
tvm.te.sum(A[x, k] * B[k, y], axis=k)
tvm.tir.Reduce(…) # expr.py
return Reduce(…); # expr.cc
生成的数据结构与上面vecadd例子中是类似的,其中Add换成了Reduce。
构建schedule
TVM中继承了Halide中algorithm与schedule分离的思想。上面定义好了算什么,接下来就需要确定怎么算了。这就是schedule要定义的事。首先,需要创建一个schedule:
s = tvm.te.create_schedule(C.op)
其中C.op类型为te.tensor.ComputeOp,返回的变量s类型为te.schedule.Schedule。基本流程如下:
create_schedule(ops) # in schedule.py
return ffi_api.CreateSchedule(ops)
create_schedule(ops) // schedule.h
return Schedule(ops) // schedule_lang.cc
auto n = make_object();
data
= n;
n->outputs = ops;
auto g = te::CreateReadGraph(n->outputs); # graph.cc
Array post_order = te::PostDFSOrder(n->outputs, g); // graph.cc
for op in post_order:
Stage stage(op);
n->stages.push_back(stage);
n->stage_map.Set(op, stage);

这里从Python调用到C++,主要作用是创建Schedule对象。构造函数中几个主要步骤:

  1. 创建相应的ScheduleNode对象,将参数中传入的Operation数组,设置到成员outputs中。对于上面的例子,Schedule()函数传入的参数中Operation数组的size为1,即ComputeOp。
  2. CreateReadGraph()函数返回ReadGraph对象,包含了输出依赖的所有操作及对应的张量。实质是一个Operation到该Operation的输入tensor的数组Array的映射。构建过程主要是以输入节点为root,然后通过Operation的InputTensors()函数,找出对应的输入tensor。上面例子就是:
    在这里插入图片描述

调用PostDFSOrder()函数得到后序的Operation数组。对于该例子便是A, B, C。表示了各个Operation之间的依赖关系。
按照上面得到的后序数组,对每个Operation创建相应的Stage对象。Schedule对象包含一系列Stage。每个Stage对象对应一个Operation。如上面的例子,就有三个Stage。每个Stage保存了一个循环嵌套(Loop nest)结构的信息,及每个循环的类型(如parallel, vectorized, unrolled)等。
创建了Schedule及对应的Stage对象后,接下来就可以进行一些操作。对于该schedule,可以应用一些调度原语(Schedule primitive)。详细可见官方文档Schedule Primitives in TVM 。下面是一个很常用的split的简单例子:
outer, inner = s[C].split(C.op.axis[0], factor=64)
上面的语句中,s[C]从schedule中得到对应的Stage对象,类型为tvm.te.schedule.Stage。split()函数第一个参数和返回值的类型都是tir.expr.IterVar,对应相应的循环变量(或者说计算轴)。将操作C的计算中的轴,以64为因子进行分割,将一重循环分成二重循环。例如,如果原来的循环次数为1024,分割后就是外循环16次,内循环64次。大体流程如下:
Stage::split() // schedule.py
outer, inner = _ffi_api.StageSplitByFactor(…) // schedule_lang.cc
IterVar outer, inner;
Stage::split(parent, factor, &outer, &inner);
SplitHelper(opertor->(), parent, factor, PrimExpr(), p_outer, p_inner);
IterVar outer = IterVar(…);
IterVar inner = IterVar(…);

            size_t pos = FindLeafVar(...);self->relations.push_back(Split(parent, outer, inner, factor, nparts))auto n = make_object<SplitNode>();...data_ = std::move(n);all_vars.push_back(outer);all_vars.push_back(inner);leaf_vars.erase(leaf_vars.begin() + pos);leaf_vars.insert(leaf_vars.begin() + pos, inner);leaf_vars.insert(leaf_vars.begin() + pos, outer);return Array<IterVar>({outer, inner});
return outer, inner;

前面提到,循环结构表示在StageNode类中。其中主要的几个相关成员:
 relations(类型Array):如这里创建的SplitNode继承自IterVarRelationNode,几个成员(parent, outer, inner, factor, nparts)描述了split的参数及前后计算轴变量。
 all_vars(类型为Array):所有的循环变量。包括split过程中所有新老循环变量。
 leaf_vars(类型为Array):当前生效的循环变量。如在这个例子中,只有经过split后的两个循环变量。
经过split过后,循环变量关系通过TEDD可视化如下:
在这里插入图片描述

主要工作在SplitHelper()函数中完成。主要步骤:

  1. 原循环变量(用IterVar表示)按照给定因子,经过切分成为两个,分别为外循环和内循环两个。如示例中,外循环范围为[0,16),内循环范围范围为[0,64)。
  2. 通过FindLeafVar()函数找到父循环变量(即split前)在leaf_vars数组中的位置,一会split后的新循环变量会插在这个位置。
  3. 创建Split对象并存入成员relations中,对应SplitNode类。保存了使用了何种调度原语(这里是split),以及应用调度原语前后的循环变量间的关系。
  4. 更新all_vars与leaf_vars两个IterVar数组。前者表示所有的(即split前后)循环变量,后者表示split后循环变量,可以理解为目前生效的循环变量。添加新产生的循环变量到all_vars和leaf_vars中,同时删除leaf_vars中的原有循环变量。
    主要数据结构如下:
    在这里插入图片描述

相关主要类简图:
在这里插入图片描述

构建的schedule,通过TEDD可视化如下:
在这里插入图片描述

经过split后,让外循环并行提高性能。可以用下面的调度原语:
s[C].parallel(outer)
调用大体流程如下:

Stage::paralle() // schedule.py
_ffi_api.StageParallel(self, var)
Stage::parallel() // schedule_lang.cc
SetAttrIterType(operator->(), var, kParallelized);
UpdateIterVarAttr(self, var, …);
ObjectPtr n = make_object();
n->iter_type = kParallelized;
self->iter_var_attrs.Set(var, IterVarAttr(n));
与上面类似,也是从Python层调用到C++层,完成实质的工作。只要设置循环变量属性就行,因此比较简单,函数UpdateIterVarAttr()中,主要就是创建相应的IterVarAttrNode对象,根据参数设置属性,最后保存到StageNode的iter_var_attrs成员中。

例如,对于常见的矩阵乘计算,通常会应用tile这个调度原语做tiling:
xo, yo, xi, yi = s[C].tile(C.op.axis[0], C.op.axis[1], 32, 32)
对于两个计算轴做tiling,对每个轴都分成外循环与内循环,然后返回总共4个新的计算轴。大体流程如下:
Stage::tile() // schedule.py
x_outer, y_outer, x_inner, y_inner = _ffi_api.StageTile(…) // schedule_lange.cc
IterVar x_outer, y_outer, x_inner, y_inner;
stage.tile(x_parent, y_parent, x_factor, y_factor, &x_outer, &y_outer, &x_inner, &y_inner);
split(x_parent, x_factor, p_x_outer, p_x_inner);
split(y_parent, y_factor, p_x_outer, p_y_inner);

reorder(Array({*p_x_outer, *p_y_outer, *p_x_inner, *p_y_inner}));
return Array({x_outer, y_outer, x_inner, y_inner);
return x_outer, y_outer, x_inner, y_inner;
其实主要的工作就是在两个维度上做split,然后对切分后的循环变量,按指定顺序做reorder。
计算的定义与schedule的构建基本就完成了。下面一下编译部分。

二.TVM调用llvm编译

前面基于一个最基本的case,介绍了TVM中计算的定义与schedule的构建。这里继续介绍接下去的一个重点部分,就是编译。
有了前面构建的schedule后,接着就需要编译生成目标代码了。这个工作主要由tvm.build()和relay.build()两个函数完成。区别在于应用目标的范围,前者用于单个算子,后者用于整个网络。由于网络可看作由算子组成,后者会调用前者。本例中是针对单个算子的,因此这里使用的是前者:
tgt = tvm.target.Target(target=“llvm”, host=“llvm”)
fadd = tvm.build(s, [A, B, C], tgt, name=“vecadd”)
其中最主要的build()函数定义在driver/build_module.py文件中。该函数基于给定参数构建出可调用的目标函数。按照官方介绍里的说法,主要做两个工作 :
 Lowering:将high-level的循环嵌套结构,转换成最终的low-level的IR。
 Codegen:从low-level的IR生成目标机器代码。
该函数的第一个参数是前面构建出来的schedule,第二个参数是函数的参数列表,第三个参数是target。提供用于lowering和codegen所需的目标平台信息。代码中对应的Target对象定义在target.*文件中。构造函数有两个参数,第一个参数target指示目标平台的配置。配置项如:
kind: 平台类型,基本决定了生成的代码是在什么处理器上运行。注册的target kind详细见target_kind.cc,有llvm, c, cuda, nvptx, romc, opencl, metal, vulkan, hexagon等。
keys: 如kind是opencl的话,key可以是mali, opencl, gpu。
device:对应实际运行的设备,会添加到keys后面。
libs:外部库,如cblas, cudnn, cublas, mkl这些。

另外,参数host与target类似,但用于指示host平台。如果taret平台为cuda,毕竟GPU还是不能完全脱离CPU运行,因此还需要host的代码做胶水,如内存分配,kernel启动这些。默认为llvm。
Lowering过程可以单独用tvm.lower()函数完成,如:
m = tvm.lower(s, [A, B, C], name=“vecadd”)
rt_mod = tvm.build(m, target=“llvm”)
也可以通过tvm.build()函数完成(因为一进去就会先调用lower()函数)。lower()函数的主要流程相关代码:
lower(sch, args, name=“main”, …) // driver/build_module.py
// Handle add_lower_pass, if any.
lower_phases0 = …

// According to the given schedule, form a function (in IRModule).
mod = form_irmodule(sch, args, …) // build_module.py
sch.normalize()
Schedule::normalize() // schedule_dataflow_rewrite.cc
InjectInline()
RebaseNonZeroMinLoop()
LegalizeInvalidAttach()
bounds = schedule.InferBound(sch)
InferBound() // bound.cc
stmt = schedule.ScheduleOps(sch, bounds)
ScheduleOps() // schedule_ops.cc
body = Stmt()
// scan init and scan updates

for each stage in schedule: // in reverse order
body = MakePipeline(stage, dom_map, body, …)
SchedulePostProc post_proc
post_proc.Init(sch)
return post_proc(body)
compact = schedule.VerifyCompactBuffer(stmt)
binds, arg_list = get_binds(args, compact, binds)
stmt = schedule.SchedulePostProcRewriteForTensorCore(stmt, sch, …)
// func type: PrimFunc
func = schedule.SchedulePostProcToPrimFunc(arg_list, stmt, …) // schedule_postproc_to_primfunc.cc
// Prepare parameters

return tie::PrimFunc(params, body, …)
// name: vecadd
func = func.with_attr(“global_symbol”, name)
// Set functions
return tvm.IRModule({name: func})

// Phase 0: InjectPrefetch, StorageFlatten, BF16Legalize, NarrowDataType, Simplify
pass_list = lower_phase0// Phase 1: LoopPartition, VectorizeLoop, InjectVirtualThread, InjectDoubleBuffer, StorageRewrite, UnrollLoop
pass_list += lower_phase1// Phase 3: Simplify, RemoveNoOp, RewriteUnsafeSelect, HoistIfThenElse
pass_list += lower_phase2// Apply the above passes.
optimize = tvm.transform.Sequential(pass_list)
mod = optimize(mod) // mod type: tvm.ir.module.IRModule
return mod 

主要根据参数给的schedule与参数生成对应的IRModule对象(定义在ir/module.h中)。IRModule是软件栈中所有IR变换的基础单元。维护函数与类型定义。这里的各种pass就是在IRModule上进行并吐出IRModule。
在这里插入图片描述

其中几个主要数据结构关系如下:

在这里插入图片描述

lower()函数中有四个阶段,第一个阶段中通过form_irmodule()函数,根据给定的schedule生成IRModule对象,然后在这个IRModule对象上,应用4轮的pass。这些pass主要分为几个阶段,分别是:
Phase 0:使用者自定义的pass。
Phase 1:使用者自定义的pass。以及:
InjectPrefetch
StorageFlatten
BF16Legalize
NarrowDataType
Simplify
Phase 2:使用者自定义的pass。以及:
LoopPartition
VectorizeLoop
InjectVirtualThread
InjectDoubleBuffer
StorageRewrite
UnrollLoop
Phase 3:使用者自定义的pass。以及:
Simplify
RemoveNoOp
RewriteUnsafeSelect
HoistIfThenElse
InstrumentBoundCheckers
这里pass其实是编译构建过程中的精华之一。
lower()函数的最后返回经过上面多轮pass优化后的IRModule对象。其中form_irmodule()函数是相对比较复杂的一部分,主要负责生成最初的IRModule对象。几个关键步骤如下:

 Schedule::normalize()函数规范化给定的schedule。主要实现在schedule_dataflow_rewrite.cc文件中。调用以下三个函数。
 InjectInline()函数处理算子内联。用到调度原语 compute_inline的话会用到。
 RebaseNonZeroMinLoop()函数将循环迭代的最小界置为0。感觉有点canonicalization的意思。
 LegalizeInvalidAttach()函数处理在使用调度原语compute_at时且目标迭代又被split或fuse情况下的合法化。
 InferBound()函数顾名思义就是边界推导(Bound inference),主要用于推导循环边界。更具体地,就是确定每个IterVar的范围,返回IterVar到Range的映射,即每个循环变量的范围。这个信息在后面的MakeLoopNest()函数中,用于确定for循环的范围,在BuildRealize()函数中设置缓冲的大小。具体可参见官方文档 InferBound Pass。
 ScheduleOps()函数基于前面经过一些处理后的Schedule对象和推导出来的循环边界产生Stmt对象。表示一个初始的循环嵌套结构。C++层中的Stmt为所有语句(Statement)的容器。子类有LetStmt,AttrStmt,AssertStmt,Store,Allocate,SeqStmt,IfThenElse,Evaluate,For,While等等。该函数会处理schedule的依赖,核心部分是逆向遍历Schedule当中的Stage(对于上面例子中就是先Compute Op,再两个Placeholder Op)。对于每个stage(PlaceholderOp除外),根据attach type调用相应的逻辑。
 对于上面的例子,Compute Op没有attach在其它计算中,因此对应Stage的attach type为kGroupRoot,因此这里调用MakePipeline()函数产生Stmt。这步比较关键比较复杂,后面再展开。
 然后通过SchedulePostProc对象(继承自StmtExprMutator),对前面生成的Stmt进行后处理。
 get_binds()函数用于绑定buffer。给每个参数张量分配buffer。如对于上面例子中的A, B, C三个张量,分别通过tvm.tir.decl_buffer(),创建buffer并绑定张量。
 SchedulePostProcToPrimFunc()函数基于ScheduleOps()产生的Stmt创建PrimFunc对象,可以用于TIR优化。PrimFunc代表包含了TIR statement的primitive function,是low-level的代码表示。
 创建IRModule对象。基于上面生成的对象封装成IRModule对象并返回。一个IRModule可以有多个函数,比较简单的情况下就一个。
上面第ScheduleOps()函数中,会调用MakePipeline()函数,针对ComputeOp对应Stage,返回一条由Stmt组成的pipeline,大体流程相关代码如下:
MakePipeline(Stage, unordered_map<IterVar, Range>, Stmt, …) // schedule_ops.cc
producer = s->op->BuildProvide(stage, …) // ComputeOpNode::BuildProvide() in compute_op.cc
ComputeType ctype = DetectComputeType(this, stage)
MakeComputeStmt(…) // compute_op.cc
ComputeLoopNest n = ComputeLoopNest::Create(…) // compute_op.cc
ComputeLoopNest ret
// make main loop nest
ret.main_nest = MakeLoopNest(stage, dom_map, …) // op_utils.cc
vector<vector> nest
nest.resize(leaf_iter_vars.size() + 1)
for iter_var in leaf_iter_vars:
nest[i + 1].emplace_back(For(var, 0, dom->extent, kind, no_op))
nest[i + 1].emplace_back(AttrStmt(iv, tir::attr::loop_scope, iv->var, no_op))

n.init_nest.emplace_back(MakeIfNest(n.init_predicates))
n.main_nest.emplace_back(MakeIfNest(n.main_predicates))
if has reduce_axis:

else:
vector provides

// Array -> SeqStmt
Stmt provide = SeqStmt::Flatten(provides) // stmt.h
provide = MergeNest(n.main_nest, provide) // ir_utils.cc
return Substitute(provide, n.main_vmap) // stmt_functor.cc
Stmt pipeline = producer
pipeline = s->op->BuildRealize(stage, dom_map, pipeline)
// set the sizes of allocated buffers
BaseComputeOpNode::BuildRealize(stage, realize_map, body) // compute_op.cc
Stmt realize = body
realize = tir::ProducerRealize(…)
pipeline = AttrStmt(s->op, tir::attr::realize_scope, …, pipeline)
return pipeline
MakePipeline()函数主要步骤如下:
 ComputeOpNode::BuildProvide()函数主要创建ComputeOp对应的循环嵌套,对应的那些Stmt对象并串成pipeline。
 首先用DetectComputeType()函数检测计算类型。遍历当前Stage的所有当前有效IterVar对象,根据属性判断计算类型,对于上面的简单例子这里为ComputeType::kNormal。
 然后根据类型调用相应函数创建Stmt对象。这里对应地是调用MakeComputeStmt()函数。
 根据Stage对象和边界推导的结果,通过ComputeLoopNest::Create()函数,创建ComputeLoopNest对象。该对象表示循环嵌套,几个主要成员:

 init_predicates与main_predicates:类型为vector。表示每个循环的边界判断,调用MakeBoundCheck()函数来生成。
 init_nest与main_nest:类型为vector<vector>。 其中main_nest是最主要的表示循环嵌套的对象,对于上面的例子,经过split后,包含两个for循环。
 根据main_predicates创建对应的Stmt(如有),用于在循环中判断该predicate是否成立,添加到main_nest结构中。
 根据有无reduce axis走不同的path。如果没有的话(如本例),对于ComputeOp的body中的每一个输出,创建ProducerStore对象,再通过MergeNest()函数将之与主嵌套main_nest合并。
 通过Substitute()函数,基于main_vmap(在MakeLoopNest()函数中准备)进行替换。

 如schedule中设置了double buffer(如s[A].double_buffer),添加对应的AttrStmt。通过增大额外的buffer,达到达到计算与访存的重叠。本例中没用到。
 如传入的consumer有定义且不是no op(指无定义、const init的EvaluateNode,或者是长度为0的SeqStmtNode),添加SeqStmt,将producer与consumer串连。本例中也不适用。
 调用BuildRealize()函数。对于每个输出的张量,在pipeline中加入ProducerRealize节点。
 最后,在pipeline中添加AttrStmt节点,标注操作的范围,返回该pipeline。
对于前面vecadd的例子,得到的pipeline大致如下示意图:
在这里插入图片描述

整个lower()函数后完成后的IR(TIR),打印出来如下:

primfn(A_1: handle, B_1: handle, C_1: handle) -> ()
attr = {“global_symbol”: “main”, “tir.noalias”: True}
buffers = {C: Buffer(C_2: Pointer(float32), float32, [1024], []),
B: Buffer(B_2: Pointer(float32), float32, [1024], []),
A: Buffer(A_2: Pointer(float32), float32, [1024], [])}
buffer_map = {A_1: A, B_1: B, C_1: C} {
for (i.outer: int32, 0, 16) {
for (i.inner: int32, 0, 64) {
C_2[((i.outer64) + i.inner)] = ((float32)A_2[((i.outer64) + i.inner)] + (float32)B_2[((i.outer*64) + i.inner)])
}
}
}
Lowering完成后,接下去就是build了。Build的主要流程相关代码如下:
build() # driver/build_module.py
input_mod = lower(inputs, args, …)

mod_host_all = tvm.IRModule()for tar, input_mod in target_input_mod.items():# build the lowered functions for a device with the given compilationmod_host, mdev = _build_for_device(input_mod, tar, target_host)# input_mod type: IRModulemod_mixed = input_mod # Apply passes:  ThreadSync, InferFragment, LowerThreadAllreduce, MakePackedAPI, SplitHostDevice...# Device optimizations: Filter, LowerWarpMemory, ,Simplify, LowerDeviceStorageAccessInfo, LowerIntrin...mod_dev = opt_device(mod_mixed) # IRModule# Host optimization: LowerTVMBuiltin, LowerDeviceStorageAccessInfo, CustomDataType, LowerIntrin, CombineContextCall...mod_host = opt_host(mod_mixed) # IRModule# Build IRModule into Module# If there are dev functionsrt_mod_dev = codegen.build_module(mod_dev, target) # target/codegen.py_ffi_api.Build(mod, target) # codegen.py# mod_host type: IRModule, rt_mod_dev type: Modulereturn mod_host, rt_mod_dev mod_host_all.update(mod_host)# Insert functions in another Module to current one_ffi_api.Module_Update()IRModuleNode::Update() # ir/module.ccdevice_modules.append(mdev)
# Generate a unified host module (type: runtime.Module)
rt_mod_host = codegen.build_module(mod_host_all, target_host)# Create LLVMModuleNode and return the corresponding Module_ffi_api.Build(mod, target) # target/codegen.cc
# Import all modules
for mdev in device_modules:rt_mod_host.import_module(mdev)_LIB.TVMModImport(mod, dep) # c_runtime_api.ccGetModuleNode(mod)->Import(...) # runtime/module.ccimports_.emplace_back(...)
return rt_mod_host # runtime.module.Module

target_input_mod包含了前面lowering输出的需要编译的IRModule及相应的target信息。如LLVM(CPU)为target,就是:{“llvm -keys=cpu -link-params=0”, IRModule}。如cuda为target,可能就是{“cuda -keys=cuda,gpu -max_num_threads=1024 -thread_warp_size=32", IRModule}。对于简单的case,target_input_mod只包含一个元素,_build_for_device()函数返回host端的IRModule,以及target端的Module(如cuda平台C++层对应CUDAModuleNode对象)。然后将host端IRModule生成一个统一的host模块,再将前面生成的对应target的Module导入其中。
这里mod_host_all与mod_host的类型为tvm.ir.module.IRModule。rt_mod_host与mdev的类型为tvm.runtime.module.Module。注意mdev只有当目标为非CPU(如GPU等)平台时才会有,当target为llvm(即for CPU)时mdev为空。
这个流程大体示意图如下:
在这里插入图片描述

其中比较核心和重要的部分是Build()函数,实现在codegen.cc文件中。会调用到具体后端的编译函数,进行目标代码生成。如cuda平台对应函数定义在build_cuda_on.cc文件中,llvm在llvm_module.cc文件中。以llvm后端为例,主要流程相关代码为:
TVM_REGISTER_GLOBAL(“target.build.llvm”)
.set_body_typed([](IRModule mod, Target target) -> runtime::Module {
auto n = make_object();
n->Init(mod, target); // llvm_module.cc
InitializeLLVM();
llvm::InitializeAllTargetInfos();
llvm::InitializeAllTargets();

unique_ptr cg = CodeGenLLVM::Create(…) // codegen_llvm.cc
// Call the corresponding codegen backend according to the target.
const PackedFunc* f = runtime::Registry::Get(“tvm.codegen.llvm.target_” + target);
handle = (*f)()
return unique_ptr(handle);

        vector<PrimFunc> funcs;for kv : mod->functions:...f = Downcast<PrimFunc>(kv.second);if (f->HasNonzeroAttr(tir::attr::kIsEntryFunc))entry_func = global_symbol.value();funcs.push_back(f);cg->Init("TVMMod", ...);CodeGenCPU::Init() // codegen_cpu.ccCodeGenLLVM::Init() // codegen_llvm.ccfor f in funcs:cg->AddFunction(f); // codegen_cpu.ccCodeGenLLVM::AddFunction();AddFunctionInternal(f);llvm::FunctionType* ftype = llvm::FunctionType::get(...);// kGlobalSymbol: "global_symbol"global_symbol = f->GetAttr<String>(tvm::attr::kGlobalSymbol);function_ = llvm::Function::Create(...);llvm::BasicBlock* entry = llvm::BasicBlock::Create(..., function_);IRBuilder::SetInsertPoint(entry);this->VisitStmt(f->body);builder_->CreateRet(ConstInt32(0));if entry_func.length() != 0:cg->AddMainFunction(entry_func); // codegen_cpu.cc// tvm_module_main : "__tvm_main__"llvm::GlobalVariable* global = new llvm::GlobalVariable(*module_, ..., tvm_module_main);global->setInitializer(llvm::ConstantDataArray::getString(*ctx_, entry_func_name))global->setDLLStorageClass(llvm::GlobalVariable::DLLExportStorageClass);module_ = cg->Finish(); // CodeGenCPU::Finish() in codegen_cpu.ccCodeGenLLVM::Finish(); // codegen_llvm.ccCodeGenCPU::AddStartupFunction();function_ = llvm::Function::Create(ftype, llvm::Function::InternalLinkage,"__tvm_module_startup", module_.get());llvm::BasicBlock* startup_entry = llvm::BasicBlock::Create(*ctx_, "entry", function_);llvm::appendToGlobalCtors(*module_, function_, 65535);builder_->CreateRet(nullptr);CodeGenLLVM::Optimize(); // codegen_llvm.cc// Function pass managerFPassManager fpass(module_.get());// Module pass managerMPassManager mpass;mpass.add(llvm::createTargetTransformInfoWrapperPass(getTargetIRAnalysis()));fpass.add(llvm::createTargetTransformInfoWrapperPass(getTargetIRAnalysis()));llvm::PassManagerBuilder builder;builder.Inliner = llvm::createFunctionInliningPass(builder.OptLevel, ...);builder.LoopVectorize = true; builder.SLPVectorize = true; ...// Run the function passesfor mod in module_:fpass.run(mod);fpass.doFinalization();// Run the module passes.mpass.run(*module_);return runtime::Module(n);
});

该函数中先创建LLVMModuleNode对象,然后调用Init()函数进行初始化,最后封装成Module对象返回。其中的Init()函数主要是将生成的TIR转为LLVM IR。主要分几步:
 InitializeLLVM()函数初始化LLVM环境。这里边主要是例行调用LLVM的一大堆初始化函数。
 创建用于代码生成的CodeGenLLVM对象。这里由于target字符串为x86-64,因此工厂函数名为tvm.codegen.llvm.target_x86-64。该工厂函数中创建CodeGenX86_64对象。因为继承关系为CodeGenX86_64 -> CodeGenCPU -> CodeGenLLVM,所以返回的是CodeGenLLVM的指针。

 类型为IRModule的参数mod中的functions成员包含了该模块中的函数。这一步中将这些函数存于类型PrimFunc的数组funcs中。对于标为入口函数(kIsEntryFunc)的函数,记录在entry_func变量中。
 接下来初始化前面创建的CodeGenX86_64对象。先调用CodeGenCPU::Init(),里边又会调用到CodeGenLLVM::Init()。前者主要创建一堆TVM运行时类型与函数。后者创建一些llvm中用于codegen的对象,如IRBuilder、llvm::Module和llvm::MDBuilder。
 对前面放入funcs数组的每个函数,调用CodeGenCPU::AddFunction()函数,进行代码生成。对本文涉及的case只有一个函数就是vecadd()。
 首先产生llvm::Function和llvm::BasicBlock对象,分别对应函数与基本块。前面在loewr()函数中将函数的名为global_symbol的属性设为相应的函数名(如vecadd)。这里将该属性取出,作为生成函数的链接时的symbol。
 通过VisitStmt()函数遍历IRModule中的各节点并转为LLVM中对应的数据结构,生成LLVM IR。这是最关键的一步了。前面构建起的TIR主要就是为了这里的转换。例如,对于ForNode就会调用CodeGenLLVM::VisitStmt_(ForNode *op)函数。会调用CreateSerialFor()函数,产生相应的LLVM IR。在优化pass中的MakePackedAPI(make_packed_api.cc)会添加一个AttrStmt,对应一个值为目标函数名加_compute_后缀的compute_scope。这样,在code generation时,CodeGenCPU::CreateComputeScope()函数(为什么加compute_scope在该函数的注释中有提到)调用。
 因此,最终的binary(可通过fadd.export_library(“vecadd.so”)语句导出)中大概会是这个样子:
在这里插入图片描述

 AddMainFunction()函数设置主函数。如上面的例子中只有一个函数vecadd(),主函数。这个symbol会放在runtime::symbol::tvm_module_main(即__tvm_main__)这个全局变量中。可以拿编译好binary验证这一点。用objdump命令dump导出的so文件,可以看到如下这段。如果将里边的0x766563616464的16进制转为ASCII,就是主函数的symbol名:vecadd。
0000000000003c87 <tvm_main>:
3c87: 76 65 jbe 3cee <__GNU_EH_FRAME_HDR+0x5e>
3c89: 63 61 64 movslq 0x64(%rcx),%esp
3c8c: 64 fs
 最后,调用CodeGenCPU::Finish()函数将LLVM IR生成后端代码。实际调用CodeGenLLVM::Finish()函数,会调用CodeGenLLVM::Finish()函数。主要调用CodeGenCPU::AddStartupFunction()函数和CodeGenLLVM::Optimize()函数。前者创建_tvm_module_startup函数,然后将一些需要启动时调用的函数填入。后者主要利用LLVM pass做一些优化。主要是向量化和函数内联。llvm中两种自动向量化。具体可参见Auto-Vectorization in LLVM。
其实,到这里编译还没有完全结束,只是构建好了LLVM的module。剩下的事情就是交给LLVM来编译生成可执行的binary了。真正生成可执行的binary是在第一次运行时通过LazyInitJIT()函数完成。 运行时会调用到LLVMModuleNode::GetFunction()函数。当发现还未生成可执行binary时,会调用LazyInitJIT()函数。该函数通过llvm::ExecutionEngine将前面产生的llvm::Module编译成真正的(能在机器上跑的)binary。然后GetFunctionAddr()函数从中获得相应的函数指针,用于执行。

参考链接:
https://blog.csdn.net/jinzhuojun/article/details/117135551
https://blog.csdn.net/jinzhuojun/article/details/119696091
https://releases.llvm.org/12.0.0/docs/Vectorizers.html#the-slp-vectorizer

查看全文
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!

相关文章

  1. To_Heart—游记——CSP-2021

    坐标 CQ Day 0 因为需要打普及组&#xff0c;和两个同学一起先回家了。 本来以为可以逃过一天的体锻&#xff0c;结果其他两位同学二话不说就向老师申请体锻完了之后再离校。 于是气喘吁吁的拿着书包&#xff0c;气鼓鼓的推着行李箱回家了 回家后想着第二天一定会出现平衡…...

    2024/4/17 22:14:57
  2. QT5程序启动画面

    今天使用QT5写一个简单的程序启动画面&#xff0c;虽然简单&#xff0c;但是却很实用。效果就是当运行程序时&#xff0c;在显示屏中央会出现一个启动画面&#xff0c;一段时间后&#xff0c;应用程序完成初始化工作&#xff0c;启动画面隐去&#xff0c;主窗口显示。 首先新建…...

    2024/4/28 5:22:01
  3. 使用H-lua框架制作魔兽争霸地图(3-物编-物品篇2)

    上节课&#xff0c;我们已经看了作者demo里面构造的“物理学圣剑”&#xff0c;那我们也去整个装备试试看。 除了攻击力&#xff0c;我们还有哪些属性呢&#xff1f;防御力&#xff0c;生命值&#xff0c;魔法值&#xff0c;攻击速度&#xff0c;移动速度&#xff0c;三维属性…...

    2024/4/28 7:00:40
  4. 亿级流量架构之资源隔离思路与方法

    为什么要资源隔离 常见的资源,例如磁盘、网络、CPU等等,都会存在竞争的问题,在构建分布式架构时,可以将原本连接在一起的组件、模块、资源拆分开来,以便达到最大的利用效率或性能。资源隔离之后,当某一部分组件出现故障时,可以隔离故障,方便定位的同时,阻止传播,避免出现滚雪球…...

    2024/4/28 0:35:20
  5. linux下编译hadoop-3.1.1源码

    下载地址 https://archive.apache.org/dist/hadoop/common/hadoop-3.1.1/hadoop-3.1.1-src.tar.gz https://github.com/protocolbuffers/protobuf/releases/download/v2.5.0/protobuf-2.5.0.tar.gz https://github.com/Kitware/CMake/releases/download/v3.14.5/cmake-3.14.…...

    2024/4/14 20:43:00
  6. 【python】算法引入及算法特性和时间复杂度

    需求&#xff1a;如果abc1000&#xff0c;且 a^2b^2c^2(a,b,c为自然数&#xff09;&#xff0c;如何求出所有a、b、c可能的组合? 步骤&#xff1a; 第一步&#xff1a;分析需求 找到以上所有满足两个条件的abc的组合 第二步&#xff1a;设计算法 尝试abc的所有组合&#xff0c…...

    2024/4/28 11:52:40
  7. 变量的使用

    1.java定义变量的格式&#xff1a;数据类型 变量名 变量值&#xff1b; 2.说明&#xff1a; ①变量必须先声明&#xff0c;后使用 ②变量都定义在其作用域内。在作用域内&#xff0c;它是有效的。换句话说&#xff0c;出了作用域就失效了 ③同一个作用域内&#…...

    2024/4/19 17:44:11
  8. [PyQt5]高级控件4 - 选项卡QTabWidget

    文章目录效果图完整代码效果图 完整代码 import sys from PyQt5.QtWidgets import *class QTabWidgetDemo(QTabWidget):def __init__(self):super(QTabWidgetDemo, self).__init__()self.resize(400, 150)#设置窗口标题self.setWindowTitle("QTabWidgetDemo")#创建3…...

    2024/4/16 7:55:39
  9. Python入门学习笔记(5)

    今日周末安排的学习内容比较简单是元组。 元组和列表结构相似&#xff0c;区别在于元组中的元素不可以单独修改&#xff0c;二列表中的元素可以任意修改。 1.元组的创建和删除 使用赋值运算符创建 num (1,2,7,9,10) #元组用(),列表用[] anguage (Python,C#) ball &qu…...

    2024/4/28 13:53:41
  10. java知识梳理--排序

    如有不足&#xff0c;还望指正&#xff0c;不胜感激&#xff0c;共同学习。 目录 排序 常见排序算法 冒泡排序 插入排序 选择排序 希尔排序 归并排序 快速排序 排序 所谓排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些关键字的大小&#xff0c;递增或…...

    2024/4/28 5:27:45
  11. 深度学习核心技术精讲100篇(七十六)-分类-决策树

    一、决策树 所谓决策树,就是自顶而下树形的结构,每一个节点都是一个属性。用决策树解决问题就是根据数据属性一层一层做决策的过程 好处:结构清晰,模仿人类思考的流程。 以下为某商品经过推销后,收集回来的客户信息,包括居住地区、住房类型、收入、是否老客户四种属性…...

    2024/4/14 20:43:05
  12. [PyQt5]高级控件3 - 树形控件QTreeWidget

    文章目录效果图完整代码效果图 完整代码 import sys from PyQt5.QtWidgets import QTreeWidget,QTreeWidgetItem,QHBoxLayout,QWidget,QApplication,QMainWindow from PyQt5.QtGui import QIcon from PyQt5.QtCore import Qtclass QListWidgetDemo(QMainWindow):def __init__(…...

    2024/4/22 3:34:53
  13. MySQL中创建数据库并添加表

    1、创建名为&#xff08;db_mingzhu&#xff09;的数据库并进行查看 2、切换到&#xff08;db_mingzhu&#xff09;数据库&#xff0c; 3、在&#xff08;db_mingzhu&#xff09;数据库中创建表&#xff08;由于表达数据类型有错误&#xff0c;第二张截图是修改author列类型&a…...

    2024/4/19 17:29:17
  14. 前端的一些概念:JavaScript、Node.js、Ajax

    一&#xff1a;JavaScript 一种脚本语言&#xff0c;解释型语言 二&#xff1a;Node.js 是一种运行环境&#xff0c;里面包含了v8引擎 使用Node.js这种环境可以使用JavaScript脱离浏览器运行&#xff0c;从而可以编写服务端的功能 三&#xff1a;Ajax 其全称是 Asynchronous…...

    2024/4/24 14:59:24
  15. 使用H-lua框架制作魔兽争霸地图(3-物编-单位篇1)

    首先&#xff0c;我们要知道&#xff0c;物编分为哪几个类型 1、unit 单位 2、item 物品 3、ability 技能 4、buff 魔法效果 5、upgrade 科技 我们这节课就先来学习下单位的物编。 打开demo项目代码&#xff0c;我们会发现&#xff0c;demo项目中&#xff0c;作者大人已经…...

    2024/4/14 20:43:31
  16. Android开发之点击输入法外部关闭键盘点击输入法外部关闭输入法的解决方法

    在此做个笔记&#xff0c;防止遗忘&#xff0c;可能以后有需要。 先看下效果图 先讲解下思路&#xff1a; 首先在actviity里面获取到当前获取焦点的控件&#xff0c;判断当前控件是不是输入法弹起的那个EditText&#xff0c;然后再Activity的分发事件dispatchTouchEvent方法里…...

    2024/4/14 20:43:51
  17. 使用H-lua框架制作魔兽争霸地图(2-项目结构认识)

    一、框架结构 当我们下载了h-lua代码后&#xff0c;框架结构是这样的&#xff0c;对不对。 depend是框架所依赖的开发套件&#xff0c;这个我们不需要去管。 我们最需要专心的是projects下的&#xff0c;我们创建的项目。 二、项目结构 如图&#xff0c;我们的demo项目创建后…...

    2024/4/22 1:15:10
  18. MATLAB从入门到精通:MATLAB识别 自带手写数字集的CNN(LeNet5)

    一、前言 以下是博主整理的精品专栏,喜欢的小伙伴可自行订阅 R语言实战应用精讲50篇 R语言函数解析及案例实战应用 MATLAB-30天带你从入门到精通 MATLAB入门知识,函数原理解析及案例解析 python快速学习实战应用系列课程 Python函数解析及实战应用案例 MATLAB深入理解…...

    2024/4/23 23:12:07
  19. SPI、UART、I2C总线详解

    当您将微控制器连接到传感器,显示器或其他模块时,您是否考虑过这两种设备是如何相互通信的?他们到底在说什么? 事实上电子设备之间的通信就像人类之间的交流,双方都需要说相同的语言。在电子产品中,这些语言称为通信协议。首先我们将从一些基本概念入手,然后再详细说明…...

    2024/4/14 20:43:46
  20. 产品经验谈:一文讲清楚商业模式梳理怎么做?

    在今天的经营环境下, 任何一家公司都离不开要做商业模式的梳理,为什么要做商业模式的梳理?根据我的理解,至少有以下几点原因: 第一点原因是, 一般创业者开始创业,都是因为有一个好点子,而此时还不足以形成一个完整的商业闭环,这时需要围绕点子展开,系统、完整的梳理点…...

    2024/4/7 3:14:37

最新文章

  1. deepin-IDE, 体验AI编程,拿精美定制礼品!

    内容来源&#xff1a;deepin&#xff08;深度&#xff09;社区 UOS AI 已经上线半年了&#xff0c;想必很多小伙伴在这半年里都体会到了人工智能的魅力。 那你们知道&#xff0c;在 deepin-IDE 中&#xff0c;可以用 AI 写代码吗&#xff1f;deepin-IDE 结合强大的 AI 编辑代码…...

    2024/4/28 13:58:20
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/3/20 10:50:27
  3. JSON格式转换

    文章目录 1. JSON 格式2. 细节 1. JSON 格式 实体类格式&#xff1a; public class Student {public string name {get; set;}public int age {get; set;} } public class Classs {public string teacher {get; set;}public List<Student> students {get; set;} }JSON格…...

    2024/4/18 22:12:03
  4. 【物联网项目】基于ESP8266的家庭灯光与火情智能监测系统——文末完整工程资料源码

    目录 系统介绍 硬件配置 硬件连接图 系统分析与总体设计 系统硬件设计 ESP8266 WIFI开发板 人体红外传感器模块 光敏电阻传感器模块 火焰传感器模块 可燃气体传感器模块 温湿度传感器模块 OLED显示屏模块 系统软件设计 温湿度检测模块 报警模块 OLED显示模块 …...

    2024/4/27 7:45:57
  5. 第十四届蓝桥杯(八题C++ 题目+代码+注解)

    目录 题目一&#xff08;日期统计 纯暴力&#xff09;&#xff1a; 代码&#xff1a; 题目二&#xff08;01串的熵 模拟&#xff09;&#xff1a; 代码&#xff1a; 题目三&#xff08;治炼金属&#xff09;&#xff1a; 代码&#xff1a; 题目四&#xff08;飞机降落 深度…...

    2024/4/26 13:48:45
  6. 【外汇早评】美通胀数据走低,美元调整

    原标题:【外汇早评】美通胀数据走低,美元调整昨日美国方面公布了新一期的核心PCE物价指数数据,同比增长1.6%,低于前值和预期值的1.7%,距离美联储的通胀目标2%继续走低,通胀压力较低,且此前美国一季度GDP初值中的消费部分下滑明显,因此市场对美联储后续更可能降息的政策…...

    2024/4/28 13:52:11
  7. 【原油贵金属周评】原油多头拥挤,价格调整

    原标题:【原油贵金属周评】原油多头拥挤,价格调整本周国际劳动节,我们喜迎四天假期,但是整个金融市场确实流动性充沛,大事频发,各个商品波动剧烈。美国方面,在本周四凌晨公布5月份的利率决议和新闻发布会,维持联邦基金利率在2.25%-2.50%不变,符合市场预期。同时美联储…...

    2024/4/28 3:28:32
  8. 【外汇周评】靓丽非农不及疲软通胀影响

    原标题:【外汇周评】靓丽非农不及疲软通胀影响在刚结束的周五,美国方面公布了新一期的非农就业数据,大幅好于前值和预期,新增就业重新回到20万以上。具体数据: 美国4月非农就业人口变动 26.3万人,预期 19万人,前值 19.6万人。 美国4月失业率 3.6%,预期 3.8%,前值 3…...

    2024/4/26 23:05:52
  9. 【原油贵金属早评】库存继续增加,油价收跌

    原标题:【原油贵金属早评】库存继续增加,油价收跌周三清晨公布美国当周API原油库存数据,上周原油库存增加281万桶至4.692亿桶,增幅超过预期的74.4万桶。且有消息人士称,沙特阿美据悉将于6月向亚洲炼油厂额外出售更多原油,印度炼油商预计将每日获得至多20万桶的额外原油供…...

    2024/4/28 13:51:37
  10. 【外汇早评】日本央行会议纪要不改日元强势

    原标题:【外汇早评】日本央行会议纪要不改日元强势近两日日元大幅走强与近期市场风险情绪上升,避险资金回流日元有关,也与前一段时间的美日贸易谈判给日本缓冲期,日本方面对汇率问题也避免继续贬值有关。虽然今日早间日本央行公布的利率会议纪要仍然是支持宽松政策,但这符…...

    2024/4/27 17:58:04
  11. 【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响

    原标题:【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响近日伊朗局势升温,导致市场担忧影响原油供给,油价试图反弹。此时OPEC表态稳定市场。据消息人士透露,沙特6月石油出口料将低于700万桶/日,沙特已经收到石油消费国提出的6月份扩大出口的“适度要求”,沙特将满…...

    2024/4/27 14:22:49
  12. 【外汇早评】美欲与伊朗重谈协议

    原标题:【外汇早评】美欲与伊朗重谈协议美国对伊朗的制裁遭到伊朗的抗议,昨日伊朗方面提出将部分退出伊核协议。而此行为又遭到欧洲方面对伊朗的谴责和警告,伊朗外长昨日回应称,欧洲国家履行它们的义务,伊核协议就能保证存续。据传闻伊朗的导弹已经对准了以色列和美国的航…...

    2024/4/28 1:28:33
  13. 【原油贵金属早评】波动率飙升,市场情绪动荡

    原标题:【原油贵金属早评】波动率飙升,市场情绪动荡因中美贸易谈判不安情绪影响,金融市场各资产品种出现明显的波动。随着美国与中方开启第十一轮谈判之际,美国按照既定计划向中国2000亿商品征收25%的关税,市场情绪有所平复,已经开始接受这一事实。虽然波动率-恐慌指数VI…...

    2024/4/27 9:01:45
  14. 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试

    原标题:【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试美国和伊朗的局势继续升温,市场风险情绪上升,避险黄金有向上突破阻力的迹象。原油方面稍显平稳,近期美国和OPEC加大供给及市场需求回落的影响,伊朗局势并未推升油价走强。近期中美贸易谈判摩擦再度升级,美国对中…...

    2024/4/27 17:59:30
  15. 【原油贵金属早评】市场情绪继续恶化,黄金上破

    原标题:【原油贵金属早评】市场情绪继续恶化,黄金上破周初中国针对于美国加征关税的进行的反制措施引发市场情绪的大幅波动,人民币汇率出现大幅的贬值动能,金融市场受到非常明显的冲击。尤其是波动率起来之后,对于股市的表现尤其不安。隔夜美国股市出现明显的下行走势,这…...

    2024/4/25 18:39:16
  16. 【外汇早评】美伊僵持,风险情绪继续升温

    原标题:【外汇早评】美伊僵持,风险情绪继续升温昨日沙特两艘油轮再次发生爆炸事件,导致波斯湾局势进一步恶化,市场担忧美伊可能会出现摩擦生火,避险品种获得支撑,黄金和日元大幅走强。美指受中美贸易问题影响而在低位震荡。继5月12日,四艘商船在阿联酋领海附近的阿曼湾、…...

    2024/4/28 1:34:08
  17. 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势

    原标题:【原油贵金属早评】贸易冲突导致需求低迷,油价弱势近日虽然伊朗局势升温,中东地区几起油船被袭击事件影响,但油价并未走高,而是出于调整结构中。由于市场预期局势失控的可能性较低,而中美贸易问题导致的全球经济衰退风险更大,需求会持续低迷,因此油价调整压力较…...

    2024/4/26 19:03:37
  18. 氧生福地 玩美北湖(上)——为时光守候两千年

    原标题:氧生福地 玩美北湖(上)——为时光守候两千年一次说走就走的旅行,只有一张高铁票的距离~ 所以,湖南郴州,我来了~ 从广州南站出发,一个半小时就到达郴州西站了。在动车上,同时改票的南风兄和我居然被分到了一个车厢,所以一路非常愉快地聊了过来。 挺好,最起…...

    2024/4/28 1:22:35
  19. 氧生福地 玩美北湖(中)——永春梯田里的美与鲜

    原标题:氧生福地 玩美北湖(中)——永春梯田里的美与鲜一觉醒来,因为大家太爱“美”照,在柳毅山庄去寻找龙女而错过了早餐时间。近十点,向导坏坏还是带着饥肠辘辘的我们去吃郴州最富有盛名的“鱼头粉”。说这是“十二分推荐”,到郴州必吃的美食之一。 哇塞!那个味美香甜…...

    2024/4/25 18:39:14
  20. 氧生福地 玩美北湖(下)——奔跑吧骚年!

    原标题:氧生福地 玩美北湖(下)——奔跑吧骚年!让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 啊……啊……啊 两…...

    2024/4/26 23:04:58
  21. 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!

    原标题:扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!扒开伪装医用面膜,翻六倍价格宰客!当行业里的某一品项火爆了,就会有很多商家蹭热度,装逼忽悠,最近火爆朋友圈的医用面膜,被沾上了污点,到底怎么回事呢? “比普通面膜安全、效果好!痘痘、痘印、敏感肌都能用…...

    2024/4/27 23:24:42
  22. 「发现」铁皮石斛仙草之神奇功效用于医用面膜

    原标题:「发现」铁皮石斛仙草之神奇功效用于医用面膜丽彦妆铁皮石斛医用面膜|石斛多糖无菌修护补水贴19大优势: 1、铁皮石斛:自唐宋以来,一直被列为皇室贡品,铁皮石斛生于海拔1600米的悬崖峭壁之上,繁殖力差,产量极低,所以古代仅供皇室、贵族享用 2、铁皮石斛自古民间…...

    2024/4/28 5:48:52
  23. 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者

    原标题:丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者【公司简介】 广州华彬企业隶属香港华彬集团有限公司,专注美业21年,其旗下品牌: 「圣茵美」私密荷尔蒙抗衰,产后修复 「圣仪轩」私密荷尔蒙抗衰,产后修复 「花茵莳」私密荷尔蒙抗衰,产后修复 「丽彦妆」专注医学护…...

    2024/4/26 19:46:12
  24. 广州械字号面膜生产厂家OEM/ODM4项须知!

    原标题:广州械字号面膜生产厂家OEM/ODM4项须知!广州械字号面膜生产厂家OEM/ODM流程及注意事项解读: 械字号医用面膜,其实在我国并没有严格的定义,通常我们说的医美面膜指的应该是一种「医用敷料」,也就是说,医用面膜其实算作「医疗器械」的一种,又称「医用冷敷贴」。 …...

    2024/4/27 11:43:08
  25. 械字号医用眼膜缓解用眼过度到底有无作用?

    原标题:械字号医用眼膜缓解用眼过度到底有无作用?医用眼膜/械字号眼膜/医用冷敷眼贴 凝胶层为亲水高分子材料,含70%以上的水分。体表皮肤温度传导到本产品的凝胶层,热量被凝胶内水分子吸收,通过水分的蒸发带走大量的热量,可迅速地降低体表皮肤局部温度,减轻局部皮肤的灼…...

    2024/4/27 8:32:30
  26. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  27. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  28. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  29. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  30. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  31. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  32. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  33. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  34. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  35. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  36. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  37. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  38. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  39. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  40. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  41. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  42. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  43. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  44. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  45. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57