Kevin Wen's Blog


  • 首页

  • 归档

  • 标签

clDNN Introduction

发表于 2020-03-15 | 阅读次数

0x1 总体结构

clDNN(Compute Library for Deep Neural Networks)是采用OpenCL来加速DNN(Deep Neural Networks)的framework。目标平台是Intel® HD and Iris™ Pro Graphics。clDNN目前已经是Intel OpenVINO的一部分。OpenVINO还包括了其它各种硬件平台的加速库,如CPU上的加速库mklDNN等。clDNN当然也可以改造成在NVIDIA和AMD的GPU上运行,虽然这个时候的性能可能需要进一步tuning。

clDNN对DNN中有关概念进行了抽象,其中有关数据类型的层次结构如下。

这些数据类型的定义简单说明如下。

Kernel - 算子计算的OpenCL实现。
Primitive - DNN中基本运算单元,如convolution, pooling, softmax等,也就是通常所说的算子。
Data - 特殊的算子,用来表示运算过程中的参数,如weights和biases, 也指DNN的输入和输出。
Engine - DNN中运行的加速器的类型,目前只有OpenCL engine一种。
Topology - 指DNN中的graph,其中包括了primitives, data和他们之间的关系。
Program - 位于Topology和Network之间(可选项),是编译好的graph网络但是没有分配内存。
Network - 编译好的graph网络并且已经分配内存,可以运行,在编译网络的过程中,网络参数可以进行特殊的优化如fusing,data reordering等。

clDNN的执行流程图如下所示。

执行过程包括下面的步骤
a.Create Engine.
b.Declare or define primitives parameters (weights and biases) if needed.
c.Create primitives. It is required to provide name for each primitive.
d.Create topology
e.Add primitives to topology
f.Build Network from topology
h.Set Inputs data
g.Execute Network

本文后续对这些过程进行详细的说明。

0x2 LoadNetwork流程分析


LoadNetwork的执行流程如上图所示,下面详细来介绍一下其中涉及到的内容。

0x21 kernel selector

前面已经知道,clDNN是通过OpenCL来加速DNN的推理执行,就是说其中的算子是通过OpenCL来加速的,kernel就是指采用OpenCL内核实现的算子。
kernel selector提供了如何选择最适合的kernel的接口,Primitive创建kernel的时候,调用kernel selector来得到最合适的kernel。

上层不能直接操作OpenCL kernel,所以提供了对应的wrapper,这些wrapper都在下面这个目录中。
inference-engine\thirdparty\clDNN\kernel_selector\core\actual_kernels
另外wrapper还定义了kernel支持的输入和输出数据格式。

对应的OpenCL kernel的定义都在这个目录下面。
inference-engine\thirdparty\clDNN\kernel_selector\core\cl_kernels

现在我们想知道OpenCL kernel是什么时候创建的呢?通过分析代码,我们可以知道OpenCL kernel的创建是在build_program的时候通过下面的循环来实现的。

1
2
3
4
5
6
7
8
9
10
void compile_graph::run(program_impl& p) {
for (auto& node : p.get_processing_order()) {
if (!node->is_type<internal_primitive>() && !node->is_type<data>()) {
node->get_output_layout();
if (!node->is_type<data>() && !(node->is_type<mutable_data>() && node->get_dependencies().empty())) {
node->selected_impl = node->type()->choose_impl(p.get_engine(), *node);
}
}
}
}

上述代码中selected_impl的定义为primitive_impl类型的std::shared_ptr变量。
上述函数会调用到下面的create()函数。
这个函数再通过调用kernel_selector.GetBestKernels来创建最合适的OpenCL kernel。

1
2
3
4
5
6
7
8
9
10
11
static primitive_impl* create(const scale_node& arg) {
......
ew_params.layoutBased = true;
auto& kernel_selector = kernel_selector::eltwise_kernel_selector::Instance();
auto best_kernels = kernel_selector.GetBestKernels(ew_params, ew_optional_params);
auto scale = new scale_gpu(arg, best_kernels[0]);
return scale;
}

0x22 primitive封装

primitive是对前面通过kernel selector取得的kernel的封装。
其中的primitive结构体都是通过typed_primitive_gpu_impl来定义的。

clDNN Library提供了下面这些primitives,

Convolution
Fully connected (inner product)
Pooling
    average
    maximum
Normalization
    across channel
    within channel
    batch
Activation
    logistic
    tanh
    rectified linear unit (ReLU)
    softplus (softReLU)
    abs
    square
    sqrt
    linear
Softmax
Crop
Deconvolution
Depth concatenation
Eltwise
ROI pooling
Simpler NMS
Prior box
Detection output

通过对上述primitive的封装,clDNN提供了下面的topologies
Alexnet
Googlenet(v1-v3)
ResNet
VGG
faster-rCNN and other.

0x23 OpenCL接口的封装

在目录inference-engine\thirdparty\clDNN\src\gpu\下面提供了OpenCL封装的代码,这些代码对OpenCL的底层api进行了封装,方便了clDNN其他模块的调用。

其中的gpu_queue类提供了对OpenCL command queue的封装,对外提供了command queue的创建和使用的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
class gpu_queue {
public:
......
private:
uint32_t id;
std::weak_ptr<gpu_toolkit> _context;
cl::CommandQueue _command_queue;
std::atomic<uint64_t> _queue_counter{0};
std::atomic<uint64_t> _last_barrier{0};
std::shared_ptr<events_pool> _events_pool;
cl::Event _last_barrier_ev;
bool _output_event = false;
};

gpu_toolkit类提供了OpenCL操作的统一接口,其他模块只需要调用gpu_toolkit就可以实现OpenCL的相关操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class gpu_toolkit : public std::enable_shared_from_this<gpu_toolkit> {
......
private:
configuration _configuration;
cl::Device _device;
cl::Context _context;
cl_platform_id _platform_id;
device_info_internal _device_info;
bool _neo_driver = false;
kernels_cache _kernels_cache;
std::map<uint32_t, gpu_queue> _command_queues_w;
std::shared_ptr<rapidjson::Document> _device_cache;
kernels_binaries_container _binaries;
bool _serialize = false;
std::string _extensions;
struct ocl_logger;
std::unique_ptr<ocl_logger> _logger;
// returns whether a barrier has been added
std::ofstream& open_log();
std::string get_device_version() { return _device.getInfo<CL_DEVICE_VERSION>(); }
// void build_command_queues();
gpu_queue& get_command_queue(uint32_t id);
};

0x24 graph optimizer

在build_program的时候会初始化graph,然后执行graph优化,包括pre_optimize_graph和post_optimize_graph。
执行步骤都是在下面的build_program函数中完成的。

1
2
3
4
5
6
7
8
9
10
11
12
13
void program_impl::build_program(bool is_internal) {
init_graph();
{ pre_optimize_graph(is_internal); }
run_graph_compilation();
{ post_optimize_graph(is_internal); }
prepare_memory_dependencies();
engine->compile_program(*this);
if (!is_internal)
prim_info = get_current_stage_info();
cleanup();
}

下面来分析一下pre_optimize_graph和post_optimize_graph分别是如何对graph进行优化的。
graph优化是通过调用apply_opt_pass来实现的。

1
apply_opt_pass<trim_to_outputs>();

apply_opt_pass是模板函数,模板参数trim_to_outputs是继承于base_pass的优化pass。

1
2
3
4
5
6
7
class trim_to_outputs : public base_pass {
public:
trim_to_outputs() : base_pass("trimmed") {}
private:
void run(program_impl& p) override;
};

模板函数apply_opt_pass的定义如下。在模板函数中生成Pass对象,Pass对象的基类是base_pass,然后调用pass_manager的run函数执行优化操作。

1
2
3
4
5
6
7
8
9
void apply_opt_pass(base_pass& pass) { pm->run(*this, pass); }
template <class Pass, typename... Args>
typename std::enable_if<std::is_base_of<base_pass, Pass>::value &&
std::is_constructible<Pass, Args...>::value>::type
apply_opt_pass(Args&&... args) {
auto pass = Pass(std::forward<Args>(args)...);
apply_opt_pass(pass);
}

pass_manager的run函数定义如下。在run函数里会调用优化pass的run函数来执行具体的优化操作。

1
2
3
4
5
6
void pass_manager::run(program_impl& p, base_pass& pass) {
......
pass.run(p);
......
pass_count++;
}

0x25 program node创建

program_node的定义如下,每一个program_node和一个primitive_impl相对应,primitive_impl是前面提到的OpenCL kernel函数的封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
Base class for all primitives which wraps API class and extends it to be used
in graph context.
*/
struct program_node {
......
protected:
std::shared_ptr<primitive> desc;
program_impl& myprog;
std::shared_ptr<primitive_impl> selected_impl;
bool valid_output_layout = false;
layout output_layout = layout(data_types::f32, format::bfyx, tensor());
std::vector<program_node*> dependencies;
std::list<program_node*> users;

program_node的创建函数如下,创建好的node保存在nodes_map中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// create all nodes from topology primitives, add dependencies among them and create inputs list
void program_impl::prepare_nodes(topology_impl const& topology) {
auto const& topo_map = topology.get_primitives();
for (const auto& prim : topo_map) {
get_or_create(prim.second);
}
......
}
program_node& program_impl::get_or_create(std::shared_ptr<primitive> prim) {
auto itr = nodes_map.lower_bound(prim->id);
if (itr != nodes_map.end() && itr->first == prim->id)
return *itr->second;
auto new_node = prim->type->create_node(*this, prim);
nodes_map.insert(itr, {prim->id, new_node});
return *new_node;
}

typed_program_node是program_node的继承类,提供了对各种类型的program_node的封装。

1
2
3
4
5
6
7
8
template <>
struct typed_program_node<activation> : public typed_program_node_base<activation> {
using parent = typed_program_node_base<activation>;
typed_program_node(const std::shared_ptr<activation> prim, program_impl& prog) : parent(prim, prog) {
support_padding_all(true);
}
......
};

0x3 Infer流程分析

前面网络加载好了以后,下面就开始真正的推理执行了,详细的流程如下。

这个时候为了加速推理执行,如上图所示,采用了多线程的方法来提高执行的并行度,主线程把不同stage的task分配到不同的线程中去执行。
每个kernel执行的时候会调用enqueueNDRangeKernel来issue OpenCL驱动来执行计算。

我们知道一个推理网络执行的时候会有很多算子在执行,这些算子的执行在GPU上,如果每个算子执行完成以后都需要把结果从GPU读取到CPU中的话,效率会很低,这种执行模型如下所示,我们称之为sync执行模式。

clDNN中采用的是如下图所示的async执行模型,各个算子之间的同步通过event来控制,每次算子执行完成以后,不需要把数据从GPU读取到CPU中。整个流程中只需要一次GPU buffer写入操作和一次GPU buffer读取操作。

下面是clDNN中enqueue kernel的代码。从代码中我们可以看到算子在每次执行enqueueNDRangeKernel的时候,需要等待一个算子执行完成的event被触发,这样算子之间的数据同步就不需要CPU的干预了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
event_impl::ptr gpu_queue::enqueue_kernel(cl::Kernel const& kern,
cl::NDRange const& global,
cl::NDRange const& local,
std::vector<event_impl::ptr> const& deps) {
std::vector<cl::Event> dep_events;
auto dep_events_ptr = &dep_events;
if (!context()->get_configuration().host_out_of_order) {
for (auto& dep : deps)
if (auto ocl_ev = dynamic_cast<base_event*>(dep.get()))
dep_events.push_back(ocl_ev->get());
} else {
dep_events_ptr = nullptr;
sync_events(deps);
}
cl::Event ret_ev;
try {
if (!context()->get_configuration().host_out_of_order || _output_event ||
context()->get_configuration().enable_profiling) {
_command_queue.enqueueNDRangeKernel(kern, cl::NullRange, global, local, dep_events_ptr, &ret_ev);
} else {
_command_queue.enqueueNDRangeKernel(kern, cl::NullRange, global, local, dep_events_ptr, nullptr);
}
} catch (cl::Error const& err) {
throw ocl_error(err);
}
return _events_pool->get_from_base_pool(context(), ret_ev, ++_queue_counter);
}
void gpu_queue::sync_events(std::vector<event_impl::ptr> const& deps) {
bool needs_barrier = false;
......
if (needs_barrier) {
try {
if (_output_event)
_command_queue.enqueueBarrierWithWaitList(nullptr, &_last_barrier_ev);
else
_command_queue.enqueueBarrierWithWaitList(nullptr, nullptr);
} catch (cl::Error const& err) {
throw ocl_error(err);
}
_last_barrier = ++_queue_counter;
}
}

Cycles in Blender

发表于 2020-02-22 | 阅读次数

0x1 Blender简介

Blender是一款开源的跨平台全能三维动画制作软件,提供从建模、动画、材质、渲染、到音频处理、视频剪辑等一系列动画短片制作解决方案。Blender的代码大体架构如下。

如图所示,Blender内置了基于物理渲染的光线追踪渲染器Cycles, Cycles除了实现CPU处理以外,还提供了OpenCL和CUDA的GPU加速,可以大幅度提升渲染速度。
Cycles具有如下特点。Pach-Tracer渲染算法,多核CPU加速渲染,CUDA和OpenCL的GPU渲染支持,多GPU支持,CPU与GPU 混合加速渲染内核。

除了Cycles以外,Blender从2.8版本开始还提供了EEVEE渲染引擎,EEVEE同样是基于物理渲染的光线追踪渲染器,其目标是提升渲染速度,因为目前Cycles的渲染速度还是比较慢的。
本文会对Blender中Cycles的执行流程做进行分析,后续也会对EEVEE的执行流程做介绍。

0x2 Cycles中的线程模型

Cycles中的线程模型如下所示。

Cycles启动以后会创建Session线程,Session会创建一个线程池,这个线程池中的线程数量是cpu的核数。只要有需要并行处理的任务就push到线程池中的queue中,然后空闲线程从queue中取得需要处理的任务并执行。
上图中左边的线程是Session线程,中间的线程TaskScheduler是线程池中其中一个线程的执行过程,线程池中其他线程的执行流程类似。
上图最右边最上边的框中列出了创建线程池中线程的代码。中间的框列出了线程池中的空闲线程从队列中取出任务的过程。下面的框列出了如何把任务push到线程池的代码,注意这些push操作可以在Session中执行,也可以在线程池中的线程运行过程中执行。

0x3 创建Session

创建Session的流程图如下所示。

这个过程主要是构建Scene Graph的过程,Scene Graph的结构体定义如下,其中可以看到需要渲染的数据都保存在这个结构体中,包括Camera,Film,Shader,Mesh,Light,Integrator等。

其中的sync_data过程可以理解成把数据从Blender的上层模块设置到Cycles渲染器中。这样Cycles执行光线跟踪的时候就可以用到这些设置。

数据设置好了以后就可以开始render过程,这个过程是采用光线跟踪的方式渲染出图像。
这个过程中需要关注的是ShaderManager, ShaderManager管理着ShaderGraph,ShaderGraph管理着Node,并且把多个Node连接起来,组成特定操作的Pipeline.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class Scene {
public:
/* Optional name. Is used for logging and reporting. */
string name;
/* data */
Camera *camera;
Camera *dicing_camera;
LookupTables *lookup_tables;
Film *film;
Background *background;
Integrator *integrator;
/* data lists */
vector<Object *> objects;
vector<Mesh *> meshes;
vector<Shader *> shaders;
vector<Light *> lights;
vector<ParticleSystem *> particle_systems;
/* data managers */
ImageManager *image_manager;
LightManager *light_manager;
ShaderManager *shader_manager;
MeshManager *mesh_manager;
ObjectManager *object_manager;
ParticleSystemManager *particle_system_manager;
CurveSystemManager *curve_system_manager;
BakeManager *bake_manager;
/* default shaders */
Shader *default_surface;
Shader *default_light;
Shader *default_background;
Shader *default_empty;
/* device */
Device *device;
DeviceScene dscene;
/* parameters */
SceneParams params;
/* mutex must be locked manually by callers */
thread_mutex mutex;
......
};

0x4 光线跟踪的执行

光线跟踪的执行流程如下所示。

这个过程是一个典型的光线跟踪渲染过程,先构建BVH结构体用于后续的加速,然后把一幅图像划分成tile的形式进行渲染,每个线程只处理一个tile,当然每一个位置还需要根据设置的sample的数量进行多次处理,最后根据多次处理的结果生成每一个位置对应的最后的像素值。
这里的svm指的是shader virtual machine, svm对shader的执行进行了抽象,把shader中的操作提练成一个个op的形式,通过对op执行的模拟来执行shader,这些op可以理解成是shader的IR(Intermediate Representation).

The lifecycle of opt_gemm in tvm

发表于 2020-01-12 | 阅读次数

0x0 简介

TVM是目前比较热门的深度学习编译框架,本文对tvm的函数注册机制和执行机制进行介绍,这些函数作为在python和c++代码之间交互的接口,可以理解成python和c++之间有统一的command交互接口。理解了这个交互接口,对理解tvm从python到c++的完整执行流程有很大的帮助。
另外本文后面以opt_gemm.py为例子,对tvm的从python到c++的执行流程进行了分析。

0x1 函数注册

TVM中所有上下层交互函数都封装到PackedFunc中,并且所有函数都保存在下面所示的Manager中,每一个函数都封装在Registry中,通过string作为key可以找到注册的函数并调用。tvm中使用的函数为什么要这样设计呢?好处是可以从python调用到这些函数的时候可以用统一的接口,简化接口层代码的编写,只需要把函数名作为key从Manager中找到对应的函数并调用。

1
2
3
4
5
6
7
8
// fmap以函数名称为key,保存函数列表
struct Registry::Manager {
std::unordered_map<std::string, Registry*> fmap;
// mutex
std::mutex mutex;
Manager() {
}

Registry的定义如下,Registry提供接口把函数都封装到PackedFunc类型的变量中,后面会详细介绍这些接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Registry {
public:
Registry& set_body(PackedFunc::FType f) { // NOLINT(*)
return set_body(PackedFunc(f));
}
template<typename FLambda>
Registry& set_body_typed(FLambda f) {
using FType = typename detail::function_signature<FLambda>::FType;
return set_body(TypedPackedFunc<FType>(std::move(f)).packed());
}
template<typename T, typename R, typename ...Args>
Registry& set_body_method(R (T::*f)(Args...) const) {
auto fwrap = [f](const T target, Args... params) -> R {
// call method pointer
return (target.*f)(params...);
};
return set_body(TypedPackedFunc<R(const T, Args...)>(fwrap));
}
template<typename TObjectRef, typename TNode, typename R, typename ...Args,
typename = typename std::enable_if<std::is_base_of<ObjectRef, TObjectRef>::value>::type>
Registry& set_body_method(R (TNode::*f)(Args...)) {
auto fwrap = [f](TObjectRef ref, Args... params) {
TNode* target = ref.operator->();
// call method pointer
return (target->*f)(params...);
};
return set_body(TypedPackedFunc<R(TObjectRef, Args...)>(fwrap));
}
protected:
/*! \brief name of the function */
std::string name_;
/*! \brief internal packed function */
PackedFunc func_;
friend struct Manager;
};

PackedFunc的定义如下。
PackedFunc内部使用std::function来保存函数对象。
TVMArgs提供了对函数参数的封装,可以包括多个参数。
TVMRetValue提供了对函数返回值的封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*!
* \brief Packed function is a type-erased function.
* The arguments are passed by packed format.
*
* This is an useful unified interface to call generated functions,
* It is the unified function function type of TVM.
* It corresponds to TVMFunctionHandle in C runtime API.
*/
class PackedFunc {
public:
/*!
* \brief The internal std::function
* \param args The arguments to the function.
* \param rv The return value.
*/
using FType = std::function<void (TVMArgs args, TVMRetValue* rv)>;
/*! \brief default constructor */
PackedFunc() {}
/*! \brief constructor from null */
PackedFunc(std::nullptr_t null) {} // NOLINT(*)
/*!
* \brief constructing a packed function from a std::function.
* \param body the internal container of packed function.
*/
explicit PackedFunc(FType body) : body_(body) {}
private:
/*! \brief internal container of packed function */
FType body_;
};

PackedFunc中的函数参数TVMArgs定义如下,可以支持可变长度的函数参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*!
* \brief Union type of values
* being passed through API and function calls.
*/
typedef union {
int64_t v_int64;
double v_float64;
void* v_handle;
const char* v_str;
TVMType v_type;
TVMContext v_ctx;
} TVMValue;
/*! \brief Arguments into TVM functions. */
class TVMArgs {
public:
const TVMValue* values;
const int* type_codes;
int num_args;

下面来分析在tvm中注册执行函数的具体执行流程,包括三种方式。

0x11 通过set_body_typed的注册

下图说明了通过set_body_typed来注册函数对象到Registry::Manager的详细过程。

下面是set_body_typed注册函数对象的举例说明。
relay模块中通过set_body_typed的注册方式来注册函数_make.relu。
注意这个函数内部还通过Op::Get()来得到op operator,这个op operator的管理在本文0x14小节中介绍。

1
2
3
4
5
6
// src\relay\op\nn\nn.cc
TVM_REGISTER_GLOBAL("relay.op.nn._make.relu")
.set_body_typed([](Expr data) {
static const Op& op = Op::Get("nn.relu");
return CallNode::make(op, {data}, Attrs(), {});
});

relay模块依赖于topi层的实现,topi层中对relu的定义如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// topi\include\topi\nn.h
template <typename T>
inline tvm::Tensor relu(const tvm::Tensor& t,
T threshold = static_cast<T>(0),
std::string name = "T_relu",
std::string tag = kElementWise) {
return tvm::compute(
t->shape,
[&](const tvm::Array<tvm::Var>& i) {
auto threshold_const = tvm::make_const(t->dtype, threshold);
return tvm::max(t(i), threshold_const);
},
name,
tag);
}

最后调用到lang模块中实现的tvm::max,构造出相应的TVM IR。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src\lang\expr_operator.cc
Expr max(Expr a, Expr b) {
// inf-aware simplificaiton
using arith::is_pos_inf;
using arith::is_neg_inf;
if (is_pos_inf(a)) return a;
if (is_neg_inf(a)) return b;
if (is_pos_inf(b)) return b;
if (is_neg_inf(b)) return a;
BinaryOpMatchTypes(a, b);
Expr ret = arith::TryConstFold<ir::Max>(a, b);
if (ret.defined()) return ret;
return ir::Max::make(a, b);
}

最后python模块中的relay层对relu的实现提供了python封装。这样python就可以调用到前面介绍的C++模块中的对应实现。

1
2
3
4
// python\tvm\relay\op\nn\nn.py
def relu(data):
"""Rectified linear unit.
return _make.relu(data)

0x12 通过set_body_method的注册

下图说明了通过set_body_method来注册函数对象到Registry::Manager的详细过程。

1
2
3
4
5
下面是一个set_body_method的调用示例。
// src\api\api_lang.cc
TVM_REGISTER_GLOBAL("_BijectiveLayoutBackwardShape")
.set_body_method(&BijectiveLayout::BackwardShape);

0x13 通过set_body的注册

下图说明了通过set_body来注册函数对象到Registry::Manager的详细过程。

下面是一个set_body的调用示例。

1
2
3
4
5
TVM_REGISTER_GLOBAL("device_api.opencl")
.set_body([](TVMArgs args, TVMRetValue* rv) {
DeviceAPI* ptr = OpenCLWorkspace::Global().get();
*rv = static_cast<void*>(ptr);
});

0x14 Relay OP注册

Relay OP保存在另外一个OpManager中,和前面的函数注册不是一个地方,这是因为这个OpManager管理的是relay operator函数,这些函数不会直接从python中调用过来。

OpManager的相关代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// src\relay\ir\op.cc
::dmlc::Registry<OpRegistry>* OpRegistry::Registry() {
return ::dmlc::Registry<OpRegistry>::Get();
}
// single manager of operator information.
struct OpManager {
// mutex to avoid registration from multiple threads.
std::mutex mutex;
// global operator counter
std::atomic<int> op_counter{0};
// storage of additional attribute table.
std::unordered_map<std::string, std::unique_ptr<GenericOpMap>> attr;
// frontend functions
std::vector<PackedFunc*> frontend_funcs;
// get singleton of the op manager
static OpManager* Global() {
static OpManager* inst = new OpManager();
return inst;
}
};
// find operator by name
const Op& Op::Get(const std::string& name) {
const OpRegistry* reg = dmlc::Registry<OpRegistry>::Find(name);
CHECK(reg != nullptr) << "Operator " << name << " is not registered";
return reg->op();
}
OpRegistry::OpRegistry() {
OpManager* mgr = OpManager::Global();
ObjectPtr<OpNode> n = make_object<OpNode>();
n->index_ = mgr->op_counter++;
op_ = Op(n);
}
template<typename EntryType>
class Registry {
}

下面是调用OpManager来注册op函数的代码示例。

1
2
3
4
5
6
7
8
9
RELAY_REGISTER_OP("argsort")
.describe(R"doc(Returns the indices that would sort an
input array along the given axis.
)doc" TVM_ADD_FILELINE)
.set_num_inputs(1)
.set_attrs_type<ArgsortAttrs>()
.add_argument("data", "Tensor", "Input data.")
.set_support_level(6)
.add_type_rel("Argsort", ArgsortRel);

如何取得OpManager中注册的op函数呢?答案是像下面这段代码所示通过调用Op::Get()来得到。

1
2
3
4
5
6
// Cache the operators that are checked
// recursively to reduce lookup overhead.
static const auto& expand_dims_op = Op::Get("expand_dims");
static const auto& reshape_op = Op::Get("reshape");
static const auto& transpose_op = Op::Get("transpose");
static const auto& squeeze_op = Op::Get("squeeze");

0x2 执行流程介绍

前面介绍了执行函数是如何注册的,那这些函数是怎样被python调用到的呢?下面来介绍一下。
我们知道从python调用过来的接口定义在c_runtime_api.h头文件中,这其中比较重要的是这两个API,TVMFuncCall()和TVMArrayAlloc,从API的名称上可知,TVMFuncCall对应于从python来的函数调用,TVMArrayAlloc对应于数组的分配。
这两个API接口定义如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// tvm\include\tvm\runtime\c_runtime_api.h
TVM_DLL int TVMFuncCall(TVMFunctionHandle func,
TVMValue* arg_values,
int* type_codes,
int num_args,
TVMValue* ret_val,
int* ret_type_code);
TVM_DLL int TVMArrayAlloc(const tvm_index_t* shape,
int ndim,
int dtype_code,
int dtype_bits,
int dtype_lanes,
int device_type,
int device_id,
TVMArrayHandle* out);

然而python是如何知道前面的函数Manager中包括了哪些函数呢?这个时候Manager提供了一个函数供上层来调用得到所有注册函数名称列表,这个函数的定义如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
int TVMFuncListGlobalNames(int *out_size,
const char*** out_array) {
API_BEGIN();
TVMFuncThreadLocalEntry *ret = TVMFuncThreadLocalStore::Get();
ret->ret_vec_str = tvm::runtime::Registry::ListNames();
ret->ret_vec_charp.clear();
for (size_t i = 0; i < ret->ret_vec_str.size(); ++i) {
ret->ret_vec_charp.push_back(ret->ret_vec_str[i].c_str());
}
*out_array = dmlc::BeginPtr(ret->ret_vec_charp);
*out_size = static_cast<int>(ret->ret_vec_str.size());
API_END();
}

然后python层根据前面的函数名称列表,通过下面的函数来得到每一个函数名称所对应的函数对象,python层拿到了这些函数对象以后,会在python层也创建相应的函数对象。这样python和c++层的函数操作就可以对应起来了。

1
2
3
4
5
6
7
8
9
10
11
int TVMFuncGetGlobal(const char* name, TVMFunctionHandle* out) {
API_BEGIN();
const tvm::runtime::PackedFunc* fp =
tvm::runtime::Registry::Get(name);
if (fp != nullptr) {
*out = new tvm::runtime::PackedFunc(*fp); // NOLINT(*)
} else {
*out = nullptr;
}
API_END();
}

在python中创建函数对象的代码如下,先根据函数TVMFuncGetGlobal()来查找底层的函数对象,找到以后根据返回的handle创建上层函数对象Function。

1
2
3
4
5
6
7
8
9
10
def get_global_func(name, allow_missing=False):
handle = FunctionHandle()
check_call(_LIB.TVMFuncGetGlobal(c_str(name), ctypes.byref(handle)))
if handle.value:
return Function(handle, False)
if allow_missing:
return None
raise ValueError("Cannot find global function %s" % name)

下面来介绍一下函数调用是如何从python层调用到C++层的,其中核心是要理解TVMFuncCall的调用过程。

0x21 TVMFuncCall的调用过程

Python中调用TVMFuncCall的代码如下所示,其中包括了函数参数的封装。
self.handle是python中持有的C++ PackedFunc对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class FunctionBase(object):
def __call__(self, *args):
temp_args = []
values, tcodes, num_args = _make_tvm_args(args, temp_args)
ret_val = TVMValue()
ret_tcode = ctypes.c_int()
if _LIB.TVMFuncCall(
self.handle, values, tcodes, ctypes.c_int(num_args),
ctypes.byref(ret_val), ctypes.byref(ret_tcode)) != 0:
raise get_last_ffi_error()
_ = temp_args
_ = args
return RETURN_SWITCH[ret_tcode.value](ret_val)

从Python代码调用到C++代码的入口函数如下。
函数参数func是封装好的PackedFunc对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// src\runtime\c_runtime_api.cc
int TVMFuncCall(TVMFunctionHandle func,
TVMValue* args,
int* arg_type_codes,
int num_args,
TVMValue* ret_val,
int* ret_type_code) {
API_BEGIN();
TVMRetValue rv;
(*static_cast<const PackedFunc*>(func)).CallPacked(
TVMArgs(args, arg_type_codes, num_args), &rv);
// handle return string.
if (rv.type_code() == kStr ||
rv.type_code() == kTVMType ||
rv.type_code() == kBytes) {
TVMRuntimeEntry* e = TVMAPIRuntimeStore::Get();
if (rv.type_code() != kTVMType) {
e->ret_str = *rv.ptr<std::string>();
} else {
e->ret_str = rv.operator std::string();
}
if (rv.type_code() == kBytes) {
e->ret_bytes.data = e->ret_str.c_str();
e->ret_bytes.size = e->ret_str.length();
*ret_type_code = kBytes;
ret_val->v_handle = &(e->ret_bytes);
} else {
*ret_type_code = kStr;
ret_val->v_str = e->ret_str.c_str();
}
} else {
rv.MoveToCHost(ret_val, ret_type_code);
}
API_END();
}
// tvm\include\tvm\runtime\packed_func.h
inline void PackedFunc::CallPacked(TVMArgs args, TVMRetValue* rv) const {
body_(args, rv);
}

下面执行对body_的调用,利用可变参数模板的递归展开来实现,这样就可以调用到真正的注册函数了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// include\tvm\runtime\packed_func.h
template<typename R, typename ...Args>
template<typename FType>
inline void TypedPackedFunc<R(Args...)>::AssignTypedLambda(FType flambda) {
packed_ = PackedFunc([flambda](const TVMArgs& args, TVMRetValue* rv) {
detail::unpack_call<R, sizeof...(Args)>(flambda, args, rv);
});
}
template<typename R, int nargs, typename F>
inline void unpack_call(const F& f, const TVMArgs& args, TVMRetValue* rv) {
unpack_call_dispatcher<R, nargs, 0, F>::run(f, args, rv);
}
template<typename R, int nleft, int index, typename F>
struct unpack_call_dispatcher {
template<typename ...Args>
static void run(const F& f,
const TVMArgs& args_pack,
TVMRetValue* rv,
Args&&... unpacked_args) {
unpack_call_dispatcher<R, nleft - 1, index + 1, F>
::run(f, args_pack, rv,
std::forward<Args>(unpacked_args)...,
args_pack[index]);
}
};
template<typename R, int index, typename F>
struct unpack_call_dispatcher<R, 0, index, F> {
template<typename ...Args>
static void run(const F& f,
const TVMArgs& args_pack,
TVMRetValue* rv,
Args&&... unpacked_args) {
*rv = R(f(std::forward<Args>(unpacked_args)...));
}
};

最后可以看到调用的是Variable::make来生成tvm IR。

1
2
3
4
5
// src\api\api_ir.cc
TVM_REGISTER_GLOBAL("_Var")
.set_body_typed([](std::string s, DataType t) {
return Variable::make(t, s);
});

0x22 TVMArrayAlloc的调用过程

TVMArrayAlloc的调用过程比较简单,直接调用NDArray的接口来分配Array。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int TVMArrayAlloc(const tvm_index_t* shape,
int ndim,
int dtype_code,
int dtype_bits,
int dtype_lanes,
int device_type,
int device_id,
TVMArrayHandle* out) {
API_BEGIN();
DLDataType dtype;
dtype.code = static_cast<uint8_t>(dtype_code);
dtype.bits = static_cast<uint8_t>(dtype_bits);
dtype.lanes = static_cast<uint16_t>(dtype_lanes);
DLContext ctx;
ctx.device_type = static_cast<DLDeviceType>(device_type);
ctx.device_id = device_id;
*out = NDArray::Internal::MoveToFFIHandle(
NDArray::Empty(std::vector<int64_t>(shape, shape + ndim), dtype, ctx));
API_END();
}

0x03 opt_gemm.py执行流程分析

下面来分析tvm自带的矩阵优化测试程序opt_gemm.py的执行流程。

0x31 根据算法创建数据流图

python代码如下,这部分描述了算法是两个矩阵相乘,根据矩阵的大小分配了相应的占位符placeholder,placeholder和tensorflow中的概念类似。然后调用compute()函数创建tvm IR。

1
2
3
4
5
6
7
8
9
10
11
M = 1024
K = 1024
N = 1024
k = tvm.reduce_axis((0, K), 'k')
A = tvm.placeholder((M, K), name='A')
B = tvm.placeholder((K, N), name='B')
C = tvm.compute(
(M, N),
lambda x, y: tvm.sum(A[x, k] * B[k, y], axis=k),
name='C')

上面算法描述对应到C++代码中,会创建相应的tvm node,这部分可以理解成是tvm的IR的生成。
下面的代码描述了上述python流程执行的最后创建ComputeOpNode的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Operation ComputeOpNode::make(std::string name,
std::string tag,
Map<std::string, ObjectRef> attrs,
Array<IterVar> axis,
Array<Expr> body) {
if (!attrs.defined()) {
attrs = Map<std::string, ObjectRef>();
}
auto n = make_object<ComputeOpNode>();
n->name = std::move(name);
n->tag = std::move(tag);
n->attrs = std::move(attrs);
n->axis = std::move(axis);
n->body = std::move(body);
if (n->body[0]->IsInstance<ir::Reduce>()) {
const ir::Reduce* reduce = n->body[0].as<ir::Reduce>();
n->reduce_axis = reduce->axis;
}
VerifyComputeOp(n.get());
return Operation(n);
}

0x32 创建Schedule

python代码如下。

1
s = tvm.create_schedule(C.op)

对应的C++代码如下,这个时候会根据前面创建的tvm IR来生成reader graph,reader graph中描述了node之间的数据依赖关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Schedule ScheduleNode::make(Array<Operation> ops) {
auto n = make_object<ScheduleNode>();
Schedule sch(n);
n->outputs = ops;
auto g = schedule::CreateReadGraph(n->outputs);
Array<Operation> post_order = schedule::PostDFSOrder(n->outputs, g);
// output set.
std::unordered_set<Operation> output_set;
for (Operation x : ops) {
output_set.insert(x);
}
for (Operation op : post_order) {
Stage stage(op);
stage->is_output = output_set.count(op) != 0;
n->stages.push_back(stage);
n->stage_map.Set(op, stage);
// mark scan updates.
if (const ScanOpNode* scan = op.as<ScanOpNode>()) {
Array<Tensor> inputs;
for (Tensor t : scan->state_placeholder) {
inputs.push_back(t);
}
for (Tensor t : scan->inputs) {
inputs.push_back(t);
}
// Create the scan group.
Stage scan_group = sch.create_group(scan->update, inputs, false);
scan_group->attach_type = kScanUpdate;
scan_group->attach_stage = stage;
for (size_t i = 0; i < scan->update.size(); ++i) {
Stage s = n->stage_map[scan->update[i]->op];
CHECK(scan_group.same_as(s->group));
}
}
}
return sch;
}

0x32 用TVM Pass来处理schedule生成的graph

执行下面的测试代码以后会调用下面的语句来创建stmt。
func = tvm.build(s, [A, B, C], target=target, name=’mmult’)

其Python代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
def lower(sch,
args,
name="default_function",
binds=None,
simple_mode=False):
cfg = current_build_config()
add_lower_pass = cfg.add_lower_pass if cfg.add_lower_pass else []
if cfg.dump_pass_ir:
add_lower_pass = BuildConfig._dump_ir.decorate_custompass(add_lower_pass)
lower_phase0 = [x[1] for x in add_lower_pass if x[0] == 0]
lower_phase1 = [x[1] for x in add_lower_pass if x[0] == 1]
lower_phase2 = [x[1] for x in add_lower_pass if x[0] == 2]
lower_phase3 = [x[1] for x in add_lower_pass if x[0] > 2]
# Phase 0
if isinstance(sch, schedule.Schedule):
stmt = form_body(sch)
for f in lower_phase0:
stmt = f(stmt)
compact = ir_pass.VerifyCompactBuffer(stmt)
binds, arg_list = get_binds(args, compact, binds)
# Phase 1
stmt = ir_pass.RewriteForTensorCore(stmt, sch, binds)
stmt = ir_pass.StorageFlatten(stmt, binds, 64, cfg.instrument_bound_checkers)
stmt = ir_pass.CanonicalSimplify(stmt)
for f in lower_phase1:
stmt = f(stmt)
# Phase 2
if not simple_mode:
stmt = ir_pass.LoopPartition(stmt, cfg.partition_const_loop)
if cfg.disable_vectorize:
stmt = ir_pass.SkipVectorize(stmt)
else:
stmt = ir_pass.VectorizeLoop(stmt)
stmt = ir_pass.InjectVirtualThread(stmt)
stmt = ir_pass.InjectDoubleBuffer(stmt, cfg.double_buffer_split_loop)
stmt = ir_pass.StorageRewrite(stmt)
stmt = ir_pass.UnrollLoop(
stmt,
cfg.auto_unroll_max_step,
cfg.auto_unroll_max_depth,
cfg.auto_unroll_max_extent,
cfg.unroll_explicit)
for f in lower_phase2:
stmt = f(stmt)
# Phase 3
stmt = ir_pass.Simplify(stmt)
stmt = ir_pass.RemoveNoOp(stmt)
if not cfg.disable_select_rewriting:
stmt = ir_pass.RewriteUnsafeSelect(stmt)
for f in lower_phase3:
stmt = f(stmt)
# Instrument BoundCheckers
if cfg.instrument_bound_checkers:
stmt = ir_pass.InstrumentBoundCheckers(stmt)
if simple_mode:
return stmt
return ir_pass.MakeAPI(stmt, name, arg_list, 0, cfg.restricted_func)

前面的ir_pass.xxx函数调用都会对应到C++的实现,这些pass是tvm中实现的中间流程处理操作,
例如前面的函数中执行的下列python代码,
stmt = ir_pass.RemoveNoOp(stmt)
其对应的C++代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src\pass\remove_no_op.cc
// Mark the statment of each stage.
class NoOpRemover : public StmtMutator {
public:
Stmt VisitStmt_(const LetStmt* op) final {
Stmt stmt = StmtMutator::VisitStmt_(op);
op = stmt.as<LetStmt>();
return is_no_op(op->body) ? MakeEvaluate(op->value) : stmt;
}
Stmt VisitStmt_(const AttrStmt* op) final {
if (op->attr_key == "pragma_debug_skip_region") {
return MakeEvaluate(0);
}
Stmt stmt = StmtMutator::VisitStmt_(op);
op = stmt.as<AttrStmt>();
return is_no_op(op->body) ? MakeEvaluate(op->value) : stmt;
}
......
};
Stmt RemoveNoOp(Stmt stmt) {
return NoOpRemover()(std::move(stmt));
}

0x33 把前面优化过的Pass调用LLVM codegen来生成LLVM IR

python代码如下。

1
2
def build_module(lowered_func, target):
return _Build(lowered_func, target)

C++代码如下,该段代码把tvm IR翻译成LLVM IR。
在其调用的Finish()函数中还会采用LLVM PassManager对已经生成的LLVM IR进行进一步优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void Init(const Array<LoweredFunc>& funcs, std::string target) {
InitializeLLVM();
tm_ = GetLLVMTargetMachine(target);
bool system_lib = (target.find("-system-lib") != std::string::npos);
CHECK_NE(funcs.size(), 0U);
ctx_ = std::make_shared<llvm::LLVMContext>();
std::unique_ptr<CodeGenLLVM> cg = CodeGenLLVM::Create(tm_.get());
entry_func_ = funcs[0]->name;
cg->Init(funcs[0]->name, tm_.get(), ctx_.get(), system_lib, system_lib);
for (LoweredFunc f : funcs) {
cg->AddFunction(f);
}
cg->AddMainFunction(funcs[0]->name);
module_ = cg->Finish();
module_->addModuleFlag(llvm::Module::Warning, "tvm_target", llvm::MDString::get(*ctx_, target));
module_->addModuleFlag(llvm::Module::Override, "Debug Info Version",
llvm::DEBUG_METADATA_VERSION);
if (tm_->getTargetTriple().isOSDarwin()) {
module_->addModuleFlag(llvm::Module::Override, "Dwarf Version", 2);
}
std::string verify_errors_storage;
llvm::raw_string_ostream verify_errors(verify_errors_storage);
LOG_IF(FATAL, llvm::verifyModule(*module_, &verify_errors))
<< "LLVM module verification failed with the following errors: \n"
<< verify_errors.str();
target_ = target;
mptr_ = module_.get();
}

0x34 根据LLVM IR生成对应的机器指令

调用了如下python代码就触发了机器指令的生成。
func(a, b, c)

func为前面返回的LLVM IR module对应的地址,a, b, c为对应的执行参数,也就是矩阵运算的输入。
对应到C++中的下述实现,调用LLVM模块来生成对应target的机器代码。

1
2
3
4
5
6
7
8
9
10
PackedFunc WrapPackedFunc(BackendPackedCFunc faddr,
const ObjectPtr<Object>& sptr_to_self) {
return PackedFunc([faddr, sptr_to_self](TVMArgs args, TVMRetValue* rv) {
int ret = (*faddr)(
const_cast<TVMValue*>(args.values),
const_cast<int*>(args.type_codes),
args.num_args);
CHECK_EQ(ret, 0) << TVMGetLastError();
});
}

0x35 评估执行时间

python代码。

1
2
evaluator = func.time_evaluator(func.entry_name, ctx, number=1)
print('Baseline: %f' % evaluator(a, b, c).mean)

c++代码如下,调用LLVM生成的机器指令来执行具体的运算。这部分还包括了把运算调用到其他机器的rpc操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PackedFunc WrapTimeEvaluator(PackedFunc pf,
TVMContext ctx,
int number,
int repeat,
int min_repeat_ms) {
......
for (int i = 0; i < repeat; ++i) {
......
// start timing
for (int i = 0; i < number; ++i) {
pf.CallPacked(args, &temp);
}
......
}
}

Analysis of mali kernel driver

发表于 2019-12-14 | 阅读次数

0x1 硬件模块

分析Mali kernel driver代码,我们可以知道Mali Midgard硬件中包括下图所示的这些模块。下面简单介绍一下这些模块。

Shader Core指的是执行Shader的ALU单元,是可编程的运算单元。

Fixed Function Operator指的是Graphics Pipeline中诸如插值,光栅化等不可编程的硬件模块。

MMU是GPU中执行虚拟地址到物理地址转换的硬件模块。

Tiler指的是Tile Based Render中执行Tile划分的硬件模块。

Power Manager指管理GPU中各个硬件子模块的Power的硬件模块。

Job Executor,GPU User space driver根据应用执行的Graphics API来生成的Job,Job Executor是消费这些Job来驱动GPU硬件执行的状态机。

Cache,和CPU中的Cache类似,GPU中的cache也是为了提高GPU访问内存的速度与效率。

0x2 Power Manager

Mali Midgard中的Power Manager模块和SOC中的Power Manager模块关系如下图所示。

SoC中的Power Manager提供GPU硬件的Power输入。在SoC bring up阶段,经常会出现GPU不能工作的情况,这个时候需要和硬件工程师配合,检查SoC的Power输出到GPU的Power输入有没有配置好,是否没有上电,电压是否符合要求。另外经常出现的功耗问题也和GPU的Power设置相关,如suspend以后没有关闭GPU的Power,这样测量出来的功耗数据会很高。

如上图所示,GPU中包括三个可以独立控制Power的模块。分别是L2 cache,Shader cores和Tiler cores。
我们来想一下为什么要分成几个独立的Power模块呢?原因也是为了功耗的考虑。我们可以单独打开/关闭Shader cores的Power,同理对L2 cache和Tiler cores模块也是如此,这样可以根据GPU执行任务的情况灵活地控制这些模块Power的打开或者关闭。

0x3 内存分配和释放

下图说明了kernel driver中分配内存的执行流程。
这个流程是由gpu user space driver驱动的,最后调用alloc_pages从Linux系统的内存管理模块分配出内存,分配的内存返回给user space driver以后,可以写入GPU执行过程中需要的数据,包括Job数据,顶点数据,纹理数据等,注意这些数据的写入是由CPU来完成的。当数据在user space driver都准备好了以后,就可以trigger kernel driver来执行GPU硬件工作,这个时候GPU硬件需要读取前面准备好的数据,这时需要借助GPU MMU来完成地址的转换工作,否则GPU没有办法完成数据的正确读取。

前面介绍了内存是如何分配的,下面解释一下分配好的内存在被gpu user space driver填充好需要的数据以后,又回到gpu kernel driver是如何执行的呢?下图说明了执行的流程。具体的gpu job相关处理会在后面的章节中介绍。

0x4 GPU MMU

前面已经提到GPU MMU用于支持GPU对非连续内存的访问。

GPU MMU的实现和CPU用来管理内存的MMU实现机制类似,也就是提供了虚拟地址到物理地址的转换。
如果GPU中没有MMU,则GPU需要访问的物理地址空间必须是连续的,这对系统的内存管理提出了很高的要求,如在Android系统中只能使用通过ion driver分配的cma buffer(当然也可以是系统启动时候预留出来的物理连续内存, 不过这种情况不常见)。处理不好的话,很大概率会出现内存不足OOM(Out of memory)的错误。

驱动中提供了两种mmu的实现,根据硬件平台进行选择。

1
2
3
4
if (kbase_hw_has_feature(kbdev, BASE_HW_FEATURE_AARCH64_MMU))
kbdev->mmu_mode = kbase_mmu_mode_get_aarch64();
else
kbdev->mmu_mode = kbase_mmu_mode_get_lpae();

每一种mmu的实现都实现了下面的结构体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* struct kbase_mmu_mode - object containing pointer to methods invoked for
* programming the MMU, as per the MMU mode supported
* by Hw.
* @update: enable & setup/configure one of the GPU address space.
* @get_as_setup: retrieve the configuration of one of the GPU address space.
* @disable_as: disable one of the GPU address space.
* @pte_to_phy_addr: retrieve the physical address encoded in the page table entry.
* @ate_is_valid: check if the pte is a valid address translation entry
* encoding the physical address of the actual mapped page.
* @pte_is_valid: check if the pte is a valid entry encoding the physical
* address of the next lower level page table.
* @entry_set_ate: program the pte to be a valid address translation entry to
* encode the physical address of the actual page being mapped.
* @entry_set_pte: program the pte to be a valid entry to encode the physical
* address of the next lower level page table.
* @entry_invalidate: clear out or invalidate the pte.
* @flags: bitmask of MMU mode flags. Refer to KBASE_MMU_MODE_ constants.
*/
static struct kbase_mmu_mode const lpae_mode = {
.update = mmu_update,
.get_as_setup = mmu_get_as_setup,
.disable_as = mmu_disable_as,
.pte_to_phy_addr = pte_to_phy_addr,
.ate_is_valid = ate_is_valid,
.pte_is_valid = pte_is_valid,
.entry_set_ate = entry_set_ate,
.entry_set_pte = entry_set_pte,
.entry_invalidate = entry_invalidate,
.flags = 0
};

GPU MMU中的页表结构和CPU MMU的类似,也是采用了多级页表结构。

GPU MMU中page fault的处理。
在MMU中断处理函数中判断是否发生了page fault,如果是则需要分配新的page给GPU,并把新分配的page信息更新到MMU中。

0x5 GPU Cache

GPU Cache位于gpu和memory之间,用来提高内存访问速度和效率。

这里面涉及到CPU和GPU之间的cache coherency的概念,指的是硬件平台上CPU的cache和GPU的cache是否可以同步。
如上图所示,CPU需要对内存中地址为addr的内存进行写操作,如果CPU采用的是write through的cache机制,CPU对内存地址addr的修改会立即写入到内存中,如果在GPU的cache中原来保存有地址addr的cache,这个时候通过Coherent Connection机制来通知GPU,告知内存地址addr对应的cache失效了,下次GPU访问内存地址addr的内存,从GPU的cache中不能读取到内存地址addr对应的数据了(cache不命中),需要重新从内存中读取才能得到正确数据。

如果CPU采用的是write back的cache机制,CPU对地址addr修改以后不会立刻写回内存,这个时候可能大家觉得这样就没有办法通过到GPU了,其实还是有机制在这种情况下也是有办法来通过GPU去使对应的GPU cache失效的。这种机制叫“窥探(snooping)”协议,窥探协议的思想是,cache不仅仅在做内存传输的时候才和总线打交道,而是不停地在窥探总线上发生的数据交换,跟踪其他缓存在做什么。所以当CPU的cache代表CPU去读写内存时,GPU也会得到通知,这样CPU和GPU的缓存可以时刻保持同步。只要GPU或者CPU其中任何一方写了内存或者cache,对方马上就知道这块内存在它们自己的cache中对应的段已经失效,然后读取的时候需要从内存中读取。

注意在支持cache coherency的硬件平台上,上述操作是不需要软件干涉的,都是通过硬件来保证的。指ARM平台上CPU和GPU是cache coherency的。

但是如果CPU是x86的,GPU的mali的SoC平台中,要做到硬件的cache coherency比较困难,这个时候是需要软件来保证的。这样gpu driver的复杂度就增加了。

在mali 驱动中有下面三种cache coherency的设置。

1
2
3
#define COHERENCY_ACE_LITE 0
#define COHERENCY_ACE 1
#define COHERENCY_NONE 31

在驱动初始化函数kbase_device_coherency_init()中会设置cache coherency的类型。
如下所示,默认是设置成COHERENCY_NONE

1
kbdev->system_coherency = COHERENCY_NONE;

也可以通过dts来配置

1
2
3
4
5
coherency_override_dts = of_get_property(kbdev->dev->of_node,
"system-coherency",
NULL);
override_coherency = be32_to_cpup(coherency_override_dts);
kbdev->system_coherency = override_coherency;

如下代码中,在CPU更新完page directory以后,如果不是cache coherency平台,这个时候需要sync来保证cache的一致性。
注意这个时候page directoy是在CPU侧写入的,CPU写入有可能只是写到cache的write buffer中,并没有真正写入内存,需要通过dma_sync_single_for_device来保证cache的write buffer中的内容都
写入了memory,这样后续GPU访问page directoy的时候能取得正确的数据。
这里有一个疑问,就是如果GPU的cache中已经有了对应内存地址的缓存内容,在不是cache coherency的平台中,是如何通知到GPU,使其对应的cache失效的呢?这部分我的理解可能也是通过硬件总线来完成的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* kbase_mmu_sync_pgd - sync page directory to memory
* This should be called after each page directory update.
*/
static void kbase_mmu_sync_pgd(struct kbase_device *kbdev,
dma_addr_t handle, size_t size)
{
/* If page table is not coherent then ensure the gpu can read
* the pages from memory
*/
if (kbdev->system_coherency != COHERENCY_ACE)
dma_sync_single_for_device(kbdev->dev, handle, size,
DMA_TO_DEVICE);
}

如下代码所示,在security模式下关闭cache coherent,保证内存数据读写的安全性。

1
2
3
4
5
6
7
8
9
10
11
12
static void kbase_gpu_disable_coherent(struct kbase_device *kbdev)
{
lockdep_assert_held(&kbdev->hwaccess_lock);
/*
* When entering into protected mode, we must ensure that the
* GPU is not operating in coherent mode as well. This is to
* ensure that no protected memory can be leaked.
*/
if (kbdev->system_coherency == COHERENCY_ACE)
kbase_cache_set_coherency_mode(kbdev, COHERENCY_ACE_LITE);
}

我们可以从下面的代码来理解一下dma_sync_single_for_cpu和dma_sync_single_for_device的区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int kbasep_10969_workaround_clamp_coordinates(struct kbase_jd_atom *katom)
{
......
// DMA transfer is complete
copy_size = MIN(PAGE_SIZE - offset, JOB_HEADER_SIZE);
page_1 = kmap_atomic(p);
/* page_1 is a u32 pointer, offset is expressed in bytes */
page_1 += offset>>2;
kbase_sync_single_for_cpu(katom->kctx->kbdev,
kbase_dma_addr(p) + offset,
copy_size, DMA_BIDIRECTIONAL);
memcpy(dst, page_1, copy_size);
......
/* Flush CPU cache to update memory for future GPU reads*/
memcpy(page_1, dst, copy_size);
p = as_page(page_array[page_index]);
kbase_sync_single_for_device(katom->kctx->kbdev,
kbase_dma_addr(p) + offset,
copy_size, DMA_TO_DEVICE);
}

kbase_sync_single_for_cpu等同于dma_sync_single_for_cpu。用于数据从GPU到内存的DMA传送刚刚完成的情况。当DMA传输完成时,GPU已经将数据传输到内存,但是cache中可能还有老数据,为了避免CPU读取还是cache中的老数据,需要调用dma_sync_single_for_cpu,在ARM平台上相当于”invalidate”操作,也就是使cache无效的操作。从上面的代码可以,DMA传输完成后,先是调用了dma_sync_single_for_cpu,CPU再从内存地址page_1中读取数据。

kbase_sync_single_for_device等同于dma_sync_single_for_device。用于数据从内存到GPU的DMA传送开始之前的情况,在CPU往内存的DMA缓冲区写入数据之后,这个时候数据可能没有立即反映到内存的DMA缓冲区上,因为该DMA缓冲区可能带有write buffer,导致数据只是写到了write buffer中,没有写入内存的DMA缓冲区上(为什么没有立即写入到内存的DMA缓冲区上呢? 是为了等write buffer达到一定的大小以后一次写入到内存,为了提高效率)。这个时候需要调用dma_sync_single_for_device来做flush/clean操作,这样后续GPU启动DMA传输的时候可以从DMA缓冲区得到正确的数据。
如上代码所示,dma_sync_single_for_device之前调用了memcpy把数据传输到DMA缓冲区中,然后执行dma_sync_single_for_device()flush
write buffer中的数据,保证后续GPU的操作能得到正确的DMA数据。

0x6 中断处理

从下面代码中我们可以知道gpu kernel driver需要处理下面三种中断。

1
2
3
4
5
static irq_handler_t kbase_handler_table[] = {
[JOB_IRQ_TAG] = kbase_job_irq_handler,
[MMU_IRQ_TAG] = kbase_mmu_irq_handler,
[GPU_IRQ_TAG] = kbase_gpu_irq_handler,
};

下图是三种中断处理函数的执行流程。

中断的处理流程如下。

  1. 调用request_irq注册中断。
  2. 操作GPU register启动中断。
  3. GPU硬件执行完成,触发中断。
  4. 处理中断处理函数,读取GPU register来判断硬件执行情况做进一步动作。

0x7. GPU Job处理

Mali GPU Job可以理解成GPU硬件能理解的IR(中间语言)。在Broadcom V3D中的CLE(control list executor)也是类似的概念。
gpu user space driver简单来说就是把上层应用的API调用转换成Job的描述。
kernel driver拿到这些Job以后,把Job的内存地址告诉GPU硬件,GPU硬件的Job Executor就开始parse这些Job,然后驱动GPU硬件的其他模块完成渲染或者计算工作。
Job可以组成Job chain的形式,Job chain中Job的执行可以有前后关系,如果该Job中需要读取texture信息,则Job中还包括texture存储位置的地址信息。

下面的流程说明了GPU Job在kernel driver中是如何提交给GPU硬件的Job Executor的。

0x8. GPU DVFS

这部分是根据GPU的loading进行动态调整GPU的运行频率,也可以动态调整GPU Power的电压。这部分的实现依赖于Linux kernel提供的DVFS(Dynamic Voltage and Frequency Scaling)机制。
当然GPU dvfs的启用与否是根据场景来的。在功耗不敏感的场景下,如汽车娱乐系统中,GPU DVFS一般是关闭的。

0x9 Reference

mali kernel driver source code

Understanding of eglSwapBuffers

发表于 2019-11-17 | 阅读次数

0x1 eglSwapBuffers在图形系统中的作用

在开发图形程序的时候,经常有同事问是不是调用eglSwapBuffers函数以后就可以输出显示了,如果简单来说可以这么理解,但是实际上gpu驱动中eglSwapBuffers函数执行以后只是提示上层模块该输出buffer可以使用了,这个时候是把输出buffer显示到屏幕上还是输出到文件中,或者是通过网络发送到远程终端上,取决于上层图形系统的设计。另外eglSwapBuffers函数需要配合上层模块(如Android上的Surface)完成buffer管理的工作。
本文详细介绍了eglSwapBuffers函数在图形系统中是如何配合上层buffer管理模块,完成从应用绘制到屏幕显示的完整流程的。

在典型图形系统中,一般包括了如下图所示的几个模块,应用程序绘制UI,合成器把多个UI合成为一帧图像,然后Frame buffer driver把合成好的图像绘制到屏幕硬件上。

这里有几个生产者-消费者模型,
App1/App2是UI内容的生产者,Compositor是App1/App2生产出来的UI内容的消费者,Compositor本身又是Frame buffer driver输入buffer的生产者。Frame buffer driver是Compositor生产出的buffer内容的消费者。

App1分配了三块buffer供gpu绘制使用,buffer的管理可以通过BufferQueue的模块来管理,在App1的绘制线程中调用gpu driver的eglSwapBuffers函数,eglSwapBuffers函数在执行的时候首先需要从BufferQueue中取得一块可以供GPU写入的buffer,如果这个时候没有空闲的buffer,gpu driver会在这里等待,如果发生这种情况,在性能分析的时候,我们可以在systrace上看到eglSwapBuffers占用的时间较长,从上面分析的buffer模型可以看出,出现没有空闲的buffer可能是Compositor执行太慢,三块buffer都被Compositor占用了,另外的原因可能是App1中gpu driver绘制执行时间太长,三块buffer都还在等待gpu硬件的写入完成。

eglSwapBuffers申请到了空闲的buffer以后,就可以把空闲buffer的地址设置给GPU硬件,并根据App1的其他设置驱动GPU硬件开始工作。这个时候eglSwapBuffers是否需要等待GPU硬件把完整的一帧绘制完成才返回呢?答案是不需要等待,但是会给这块buffer设置相应的fence,然后把这块buffer送给后续的Compositor来使用,Compositor在需要读取这块buffer内容之前,GPU硬件可以继续完成该buffer的绘制工作,达到CPU处理(Compositor的处理流程)和GPU处理的并行,Compositor在执行到了必须读取这块buffer内容的时候,会去检查buffer对应的fence的状态,如果还没有被signal,则需要等待,直至GPU绘制完成后singal对应的fence。

0x2 应用进程使用eglSwapBuffers

App1执行流程如下所示。

  1. 应用程序调用createBufferQueue创建BufferQueue,其中包括Buffer的Producer和Consumer。Producer是buffer的生产者,Consumer是buffer的消费者。这样应用中的渲染线程相当于Producer,合成器是Consumer,是buffer的消费者。
  2. BufferQueue通过合成器进程创建buffer队列,一般创建3个buffer。
  3. 渲染线程把Producer包装成Surface,然后把Surface作为参数去调用eglCreateWindowSurface。这样GPU driver相当于Producer,负责生产buffer。
  4. 应用程序调用draw命令,渲染线程调用eglSwapBuffers。
  5. eglSwapBuffers通过dequeue()查找BufferQueue中空闲的buffer,如果没有空闲的buffer则需要等待。
  6. eglSwapBuffers把空闲的buffer的地址设置给GPU硬件,并设置其他参数,驱动GPU硬件工作。
  7. eglSwapBuffers通过queue()把前面GPU的写入buffer返回给BufferQueue,并设置相应的fence。
  8. BufferQueue通过Consumer接口通知合成器有新的buffer到来,可以进行合成工作。

0x3 合成器进程使用eglSwapBuffers

Compositor执行流程如下所示。
这里我们只讨论采用GPU来做合成的情况,如采用2D加速硬件来做合成的话就不会在合成器中调用eglSwapBuffers。

  1. 应用程序创建的时候会在合成器中创建相应的Layer,这样应用程序的绘制输出就可以通知到对应的Layer。
  2. 创建相应的DisplayDevice,在创建DisplayDevice的时候创建BufferQueue,这里Producer是合成器线程,Consumer是后面连接的Frame buffer driver。
  3. 应用程序通过其渲染线程中的eglSwapBuffers通知合成器有新的buffer到来,需要进行合成工作。
  4. 合成器线程准备开始合成,等待应用渲染线程中的buffer被GPU硬件绘制完成。
  5. 合成器线程调用eglSwapBuffers。
  6. eglSwapBuffers通过dequeue()查找BufferQueue中空闲的buffer,如果没有空闲的buffer则需要等待。
  7. eglSwapBuffers通过queue()把前面GPU的写入buffer返回给BufferQueue,并设置相应的fence。
  8. BufferQueue通过Consumer接口通知Frame buffer driver有新的buffer到来,可以把buffer内容绘制到屏幕硬件上。

0x4 图形系统中buffer管理的特点

上面分析基本是基于Android系统的,从上面的流程可知,buffer的分配是在合成器进程中分配的,然后返回给应用进程使用。
如果是采用wayland协议的weston图形系统中,buffer的分配是在应用进程中进行的,然后把相应的buffer handle传给weston进程,作为weston合成的输入buffer使用。

About Video Codec Optimization

发表于 2019-10-13 | 阅读次数

0x1 Video Codec流程

视频编码流程如下图所示。

视频解码流程如下图所示。

如何对编解码器进行优化呢?
主要方法有算法优化,指令集优化和并行优化。本文的后面部分会对这些优化方法进行详细的介绍。
这边先来介绍一下优化的量化指标。

对编解码来说,共性的指标是编解码速度和消耗的功耗。编解码速度可以用fps来量化。这个是典型的软件优化过程,这个时候,诸如降低cache miss率,循环展开等优化思想都是适用的。
对软件编解码器而言,功耗可以量化为CPU占用率或者是MIPS(Million Instructions Per Second)。对硬件编解码器来说,功耗可以量化成硬件执行频率/硬件使用率等,也可以是硬件电压和电流参数。

对编码器来说,我们还需要保证相同码率下图像质量不衰退,这个衡量指标是PSNR和码率。在编码优化过程中,如何保证优化以后不出问题呢?我们知道编码的流程中是包括解码过程的,我们可以把编码过程中解码部分输出的YUV数据保存下来,再把编码生成的码流用参考解码器进行解码并保存为参考YUV输出,这个参考YUV输出和前面编码器中保存的YUV数据进行比较,如果两者的YUV数据有差异,可以确定是编码器优化出了问题,这个时候可以比较具体的比特位差异来快速定位问题。

另外在编解码优化过程中,我们还会用到码流分析工具,如Vega,streameye和yuvviewer等。

对解码器来说,我们还需要运行各种conformance test来保证解码优化以后不引入衰退。

0x2 算法优化

算法优化主要针对编码器而言的。
编码算法优化的目标有三个,一个是在保证质量的前提下降低码率,另一个是码率不变情况下提升编码质量。第三个目标是保证图形质量和码率不变的情况下提升编码速度。

前面两个的目标也是制定编解码器标准的目标,在一种特定的码流格式确定之前需要召开多次会议来讨论码流的细节,这种会议会讨论各个单位的提案,选出最佳的coding tools,达到最佳的压缩效率。下面我们来讨论一下如何按照第三个目标来优化编码器。

编码算法进行优化后,如何保证编码质量呢?这个时候需要通过客观指标如PSNR来保证,或者是类似的主观指标如MOS或者DMOS来保证。通过比较优化前后的PSNR,我们可以知道编码质量有没有下降。如何检查码率有没有变化呢?这个很简单,直接比较编码生成的文件大小有没有增加即可。

下面以AVC的编码器为例,看看如何对编码算法进行一定的优化。

帧内预测优化
比如在AVC的Luma帧内编码过程中,对应4x4大小的block,我们需要遍历9种预测方向来找到最优的预测方式。这个时候我们可以利用像素的特点减少预测方向的数量。另外还有16x16,8x8(High Profile而言)大小需要比较,这个时候都可以通过减少预测数量来优化。

帧间预测优化
对帧间预测的模式选择过程进行优化,因为运动估计比较耗时,如果所有模式都搜索一遍很耗时,可以利用像素特点对运动估计进行简化。
还有一种是I macroblock in P frame,需要计算什么情况下要在P frame采用Intra macroblock,这个过程也可以优化,对P frame中一个特定的宏块,可以采用某种方式来判断是否需要进行采用Intra编码,而不是需要计算好Inter和Intra的cost才能通过比较cost来选择。
另外可以采用不同的运动搜索算法来对简化通过运动搜索找到最佳运动矢量的过程。

码率控制优化
码率控制的过程是选择哪个宏块采用哪个量化参数QP的过程,有frame level的码率控制, 这个时候整个frame都是采用固定的QP。有slice level的码率控制,这个时候整个slice采用固定的QP。有macroblock level的码率控制,这个时候每个宏块可以自由地调节QP。macroblock level的码率控制是最准确的,但是计算复杂度也最大,可以通过在这几种码率控制级别之间进行选择,来达到码率控制和计算复杂度之间的平衡。

解码因为是固定的流程,算法没有办法进行优化,当然也有特例,如2006年左右PC性能比较差的时候,需要播放BluRay的AVC码流,在1080P情况下AVC软件解码性能不足,CPU占用率高,某播放器公司是把AVC的deblocking关闭的,当然画面质量会受到一些影响。另外在早期SOC平台性能不足,在软件解码的时候,很多时候会把不参与预测的B帧直接丢弃。

0x3 平台指令集优化

这种优化方法是采用各个平台特有的SIMD指令对编解码过程中某些运算过程进行加速,如x86上的mmx/sse/sse2/sse3/sse4/avx/avx512等SIMD指令,ARM平台上的SIMD指令有neon指令。

如编解码过程中典型的Hadmard变换,SATD, SAD, DCT, IDCT,运动补偿插值,Deblocking等过程都是很适合采用SIMD指令来加速的。
这里特别说明一下运动补偿插值的过程,目前AVC的Luma分量支持1/4像素的插值,也就是说每个像素需要插值成15个分像素点供后续运动估计使用。如果内存充足的话,可以利用SIMD方法把15个分像素点先计算好,这样在运动估计的时候就不需要做插值工作。
如下图所示,方框所示为整像素点,圆形所示为分像素点。可以按照1/2/3, 4/8/12, 5/7/13/15, 10, 6/9/11/14的过程进行SIMD计算。

目前各个开源编解码库,如ffmpeg,x264,xvid,x265等,这些库的很大部分优化工作就是在各个平台上进行SIMD优化。

0x4 GPU并行优化

这里说的GPU并行优化一般指采用opencl/cuda之类的GPU Compute API来进行编解码的处理。

x264中采用了opencl来进行编码优化,把CPU需要完成的工作offload到GPU中来完成,其中x264采用opencl来在analysis阶段分析像素的特点来提前确定GOP中IBP帧的排列分布,还用来判断当前Slice是需要用Intra编码还是Inter编码。

0x5 并行优化

0x51 分布式优化

分布式优化一般用于编码优化,基本原理是把需要编码的文件划分成几个部分,每一部分分别在不同的机器上进行编码,编码完成以后再把编码好的几段码流合并成一个完整的码流。这种编码优化方法一般多见于专业视频制作过程,需要保证图形质量最佳的情况下码率最小,一般会启用编码器中的所有feature。另外一般采用多pass的编码方法,第一个pass先分析输入素材的特点,根据分布式处理的机器数目来确定那一段输入素材在哪台机器上进行编码。另外需要指出的是在分割点附近的码率控制算法需要特别处理,否则容易出现码率突然增大的情况。

0x52 GOP并行优化

这种优化方法是把一个GOP分配给一个线程来进行优化。
对编码而言,可以控制每个GOP中frame的数目,并且不采用Open GOP,只采用Close GOP的方式来进行编码。这样每个线程的负载可以做到比较平均。每个GOP开头和结尾处的码率控制需要特别处理。

对解码而言, 如果GOP之间的frame数目差别较大,则没有办法做到线程的负载平衡。而且如果是Open GOP的话,线程之间也有等待操作。

0x53 Frame并行优化

Frame级别优化是把Frame分配给不同的线程处理,一般用于视频解码,如果当前帧的当前宏块的解码过程需要依赖于前面帧的解码完成,这个时候是需要等待,这个在目前的视频标准中是很常见的。如P帧的解码需要依赖前面I帧的解码完成。

0x54 Slice并行优化

对编码器来说,可以按照多个Slice来并行编码,只需要在编码器启动的时候配置好Slice数目即可,需要注意的是,Slice编码完成以后,一般需要进行Deblocking,这个过程是跨Slice边界的,也就是说这个时候没有办法并行处理了,需要等待所有Slice编码完成以后在Control Thread中完成Deblocking的过程。
对解码器来说,按照码流中的slice数目启动多线程解码。
这种并行优化方式的优点是实现简单。
这种并行优化方式的缺点是,对编码器来说会稍微损失码率,对解码器来说是依赖于slice数目,如果只有一个slice,没办法并行处理。

0x55 MB Block并行优化

这种优化方式一般用于解码器。优点是线程负载比较平衡,缺点是实现较复杂,需要深入解码器内部进行划分任务,把解码任务划分成不同的阶段,如VLD,IDCT,MC,Deblocking等。每个线程只是执行一个阶段的任务,而且执行的宏块数也只是上图中一个长方框内的宏块数目。这个时候如果算法的实现中对前后方框之间的宏块有数据依赖的话,需要加入同步机制。如ffmpeg就没有实现这种方式。

0x6 AI优化

AV1的参考编码器libaom中采用了AI来加速partiton划分, partiton划分在libaom中占用了大概80%的复杂度。libaom编码过程中采用的CNN + DNN的网络是经过训练的,在libaom的代码中并没有提供这个网络结构的训练过程代码。但是从其推理过程我们可以大体知道其训练过程,其实现方式应该是设计好网络以后,通过大量样本数据来训练得到该推理网络的。

0x7 硬件优化

下面提到的DXVA和LibVA是指通过GPU集成的视频编解码能力来加速视频处理。
DXVA指Windows平台上的GPU视频编解码加速标准。
LibVA指Linux平台上的GPU视频编解码加速标准。

DXVA架构图如下所示。

LibVA架构图如下所示。

ASIC优化
还有一种优化方法是把编解码模块做成ASIC模块,如早期的WisChip公司的视频编解码器。
现在Verisilicon公司的Hantro IP, Broadcom的VideoCore, 各个手机SOC厂商(Huawei, MTK, Spreadtrum, qualcomm)在SOC中集成的视频编解码IP。

DSP优化
如TI公司的DSP平台如TMS320C64可以用来优化Codec。

FPGA优化
如Altera和Xilinx的FPGA都可以用来优化视频编解码能力。

0x8 优化思考

目前openmp, tbb等基础库已经很成熟,已经广泛应用于各种深度学习推理框架的优化中。视频编解码优化应该也可以采用这些基础库来优化。
另外是可以思考是否可以采用TVM类似框架来优化,现在对推理框架的优化热潮和十多年前对codec的优化热潮很像。如果可以有TVM类似的框架,目前视频编解码优化中各种平台上的手写SIMD优化工作应该可以通过类似的AutoTune工作来完成。这样视频编解码优化的工作可以从繁重的手写汇编的工作中解放出来,把优化侧重点放在对算法和计算流程方面的优化。

Intel Gen Driver流程介绍

发表于 2019-07-13 | 阅读次数

0x1 总体流程

GPU硬件简单说来可以包括这几块,首先是内存访问相关模块,如MMU,Cache等。其次是各种Fixed Function模块,如Rasterizer,Clipper等。最后是可编程单元模块Shader Core,Shader Core的加入使GPU具有了和CPU一样具体处理各种复杂问题的能力,赋予了用户很大的发挥空间来编写各个OpenGL shader,OpenCL kernel,这些shader和kernel最后都会生成对应的专有GPU指令并运行在Shader Core上。在Intel Gen GPU中,对应的Shader Core模块也称为EU(Execution Unit)。

从GPU driver到GPU硬件完成绘制的流程可以简单说明如下。

a. GPU user space driver在CPU侧根据应用设置的各种状态生成各种command list。

b. GPU user space driver在CPU侧通过compiler把shader源代码生成对应的gpu shader core指令. 这个shader core指令也是保存在memory中,driver会把其地址保存在某一个command结构体中。

c. GPU user space driver把前面生成的内容发送到GPU kernel space driver中。GPU kernel space driver启动gpu硬件读取command list的内容。

d. GPU中的command executor开始消费command,根据command的内容驱动gpu硬件中各个不同的模块协同工作。当需要shader core执行的时候,从相应的memory buffer中读取GPU指令,shader core根据GPU指令完成相应的shader操作。当需要读取Vertex和Texture内容的时候,从相应的command找到对应的buffer地址,然后读取相应的内容。
上面提到的流程说明可以简单地用下图所示。

本文后面用一个简单的三角形绘制测试程序说明一下GPU driver是如何工作的。这里的GPU driver主要是指开源的User space driver实现mesa。

这个测试程序的流程如下图所示。

0x2 Shader编译

下面说明一下mesa driver中Intel Gen shader编译过程。

Vertex Shader的编译过程如下。

对应的GLSL source code如下所示。
这个shader只是把外部设置的attribute信息Position设置到gl_Position中。

1
2
3
4
5
attribute vec4 vPosition;
void main()
{
gl_Position = vPosition;
}

编译的第一阶段是完成词法分析,语法分析,生成对应的抽象语法树AST。
然后根据AST生成Mesa内部的中间表示NIR。然后在NIR上执行各种编译器优化。每执行一次优化称为一个Pass。
经过多个Pass优化以后,最后生成的NIR代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
NIR (final form) for vertex shader:
shader: MESA_SHADER_VERTEX
name: GLSL3
inputs: 1
outputs: 1
uniforms: 0
shared: 0
decl_var shader_in INTERP_MODE_NONE highp vec4 vPosition (VERT_ATTRIB_GENERIC0.xyzw, 16, 0)
decl_var shader_out INTERP_MODE_NONE highp vec4 gl_Position (VARYING_SLOT_POS.xyzw, 0, 0)
decl_function main (0 params)
impl main {
block block_0:
/* preds: */
vec1 32 ssa_0 = load_const (0x00000000 /* 0.000000 */)
vec4 32 ssa_1 = intrinsic load_input (ssa_0) (0, 0, 160, 144) /* base=0 */ /* component=0 */ /* dest_type=float32 */ /* location=16 slots=1 */
intrinsic store_output (ssa_1, ssa_0) (0, 15, 0, 160, 128) /* base=0 */ /* wrmask=xyzw */ /* component=0 */ /* src_type=float32 */ /* location=0 slots=1 */ /* gl_Position */
/* succs: block_1 */
block block_1:
}

下面开始执行编译器后端,生成具体的GPU shader core指令。这里shader core也就是前面提到的EU,所以我们也就是要生成EU code。
这里面也用到了传统的编译器后端技术,如图着色寄存器分配等。
最后生成的EU code如下所示。

1
2
3
4
5
6
7
8
9
10
11
Native code for unnamed vertex shader GLSL3 (sha1 15358268e6b3974dafb7512e3bdaa7c4c81a9394)
SIMD8 shader: 6 instructions. 0 loops. 20 cycles. 0:0 spills:fills, 1 sends, scheduled with mode top-down. Promoted 0 constants. Compacted 96 to 64 bytes (33%)
START B0 (20 cycles)
mov(8) g122<1>UD g1<8,8,1>UD { align1 WE_all 1Q compacted };
mov(8) g123<1>F g2<8,8,1>F { align1 1Q compacted };
mov(8) g124<1>F g3<8,8,1>F { align1 1Q compacted };
mov(8) g125<1>F g4<8,8,1>F { align1 1Q compacted };
mov(8) g126<1>F g5<8,8,1>F { align1 1Q compacted };
send(8) null<1>F g122<8,8,1>F 0x8a080017
urb MsgDesc: 1 SIMD8 write mlen 5 rlen 0 { align1 1Q EOT };
END B0

Fragment Shader的编译过程如下。
具体的流程和Vertex Shader的编译过程类似。详细的过程不介绍了,下面只是列出各个编译阶段的结果。

GLSL source code

1
2
3
4
5
precision mediump float;
void main()
{
gl_FragColor = vec4 ( 1.0, 0.0, 0.0, 1.0 );
}

NIR code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
NIR (final form) for fragment shader:
shader: MESA_SHADER_FRAGMENT
name: GLSL3
inputs: 0
outputs: 1
uniforms: 0
shared: 0
decl_var shader_out INTERP_MODE_NONE mediump vec4 gl_FragColor (FRAG_RESULT_COLOR.xyzw, 4, 0)
decl_function main (0 params)
impl main {
block block_0:
/* preds: */
vec4 32 ssa_0 = load_const (0x3f800000 /* 1.000000 */, 0x00000000 /* 0.000000 */, 0x00000000 /* 0.000000 */, 0x3f800000 /* 1.000000 */)
vec1 32 ssa_1 = load_const (0x00000000 /* 0.000000 */)
intrinsic store_output (ssa_0, ssa_1) (4, 15, 0, 160, 8388738) /* base=4 */ /* wrmask=xyzw */ /* component=0 */ /* src_type=float32 */ /* location=2 slots=1 mediump */ /* gl_FragColor */
/* succs: block_1 */
block block_1:
}

EU code

1
2
3
4
5
6
7
8
9
10
Native code for unnamed fragment shader GLSL3 (sha1 f7554fd836ad71e30768cdc6b984e294b8f2fae5)
SIMD8 shader: 5 instructions. 0 loops. 18 cycles. 0:0 spills:fills, 1 sends, scheduled with mode top-down. Promoted 0 constants. Compacted 80 to 64 bytes (20%)
START B0 (18 cycles)
mov(8) g123<1>F 0x3f800000F /* 1F */ { align1 1Q };
mov(8) g124<1>F 0x0VF /* [0F, 0F, 0F, 0F]VF */ { align1 1Q compacted };
mov(8) g125<1>F 0x0VF /* [0F, 0F, 0F, 0F]VF */ { align1 1Q compacted };
mov(8) g126<1>F 0x3f800000F /* 1F */ { align1 1Q };
sendc(8) null<1>UW g123<0,1,0>UD 0x88031400
render MsgDesc: RT write SIMD8 LastRT Surface = 0 mlen 4 rlen 0 { align1 1Q EOT };
END B0

0x3 Command生成

Command Stream Unit是Gen GPU内部用来管理3D pipeline或者Media单元的模块,通过配置不同的Command Stream命令,我们可以精细地控制3D pipeline的运行。
Command Stream Unit还提供了URB分配和管理的功能。URB可以理解成是用来在各个Pipeline阶段(如VS, Rasterizer, Clipper, PS等)之间传递参数的buffer。

对Command Stream的编程,简单地理解就是把Graphics API的状态配置转换成Command Stream的命令。不同API函数执行的时候需要配置不同的Command Stream命令,我们可以通过command stream dump机制把生成command保存下来,然后通过可视化的工具分析出现时问题。mesa提供了可视化的viewer工具来分析生成的每个command。

对于Broadcom GPU来说,和Command Stream类似的概念叫做Control List,执行Control List的硬件模块叫做Control List Executor。Broadcom GPU驱动也需要配置Control List来驱动GPU的执行。

下面来说明一下一个简单的OpenGL ES应用执行的时候需要配置哪些Command Stream命令。

eglCreateContext调用以后,会执行driver初始化动作。需要配置下面这些Command。

GEN9_PIPE_CONTROL
GEN9_PIPE_CONTROL
GEN9_PIPELINE_SELECT
state GEN9_L3CNTLREG
GEN9_MI_LOAD_REGISTER_IMM
GEN9_PIPE_CONTROL
GEN9_STATE_BASE_ADDRESS
GEN9_PIPE_CONTROL
state GEN9_CS_DEBUG_MODE2
GEN9_MI_LOAD_REGISTER_IMM
state GEN9_CACHE_MODE_1
GEN9_MI_LOAD_REGISTER_IMM
GEN9_3DSTATE_DRAWING_RECTANGLE
GEN9_3DSTATE_SAMPLE_PATTERN
GEN9_3DSTATE_AA_LINE_PARAMETERS
GEN9_3DSTATE_WM_CHROMAKEY
GEN9_3DSTATE_WM_HZ_OP
GEN9_3DSTATE_POLY_STIPPLE_OFFSET
GEN9_3DSTATE_PUSH_CONSTANT_ALLOC_VS
GEN9_3DSTATE_PUSH_CONSTANT_ALLOC_VS
GEN9_3DSTATE_PUSH_CONSTANT_ALLOC_VS
GEN9_3DSTATE_PUSH_CONSTANT_ALLOC_VS
GEN9_3DSTATE_PUSH_CONSTANT_ALLOC_VS
GEN9_3DSTATE_CC_STATE_POINTERS
GEN9_PIPE_CONTROL
GEN9_PIPE_CONTROL
GEN9_PIPELINE_SELECT
state GEN9_L3CNTLREG
GEN9_MI_LOAD_REGISTER_IMM
GEN9_PIPE_CONTROL
GEN9_PIPE_CONTROL
GEN9_STATE_BASE_ADDRESS
GEN9_PIPE_CONTROL
GEN9_PIPE_CONTROL

eglMakeCurrent调用以后,需要给frame buffer分配Gem buffer。

glShaderSource/glCompileShader创建shader对象,把glsl source code设置到gpu driver中。

glLinkProgram调用以后,执行glsl的编译动作,把glsl source code编译成glsl AST,然后转换成NIR。

glClear调用以后,mesa driver会通过Blit engine来执行clear操作,这个时候需要给Blit engine生成EU code。所以这个时候会生成glsl fragment shader code,再转换成NIR,再生成EU code。这个时候需要配置下面这些Command。

state GEN9_GT_MODE
GEN9_PIPE_CONTROL
GEN9_MI_LOAD_REGISTER_IMM

GEN9_PIPE_CONTROL
GEN9_PIPE_CONTROL

GEN9_PIPE_CONTROL
GEN9_STATE_BASE_ADDRESS
GEN9_PIPE_CONTROL

这边再说明一下,给Blit engine生成的fragment shader的NIR和EU code如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
NIR (final form) for fragment shader:
shader: MESA_SHADER_FRAGMENT
name: BLORP-clear
inputs: 0
outputs: 0
uniforms: 0
shared: 0
decl_var shader_in INTERP_MODE_FLAT vec4 clear_color (VARYING_SLOT_VAR0.xyzw, 32, 0)
decl_var shader_out INTERP_MODE_NONE vec4 gl_FragColor (FRAG_RESULT_COLOR.xyzw, 4, 0)
decl_function main (0 params)
impl main {
block block_0:
/* preds: */
vec1 32 ssa_0 = load_const (0x00000000 /* 0.000000 */)
vec4 32 ssa_1 = intrinsic load_input (ssa_0) (32, 0, 160, 160) /* base=32 */ /* component=0 */ /* dest_type=float32 */ /* location=32 slots=1 */ /* clear_color */
intrinsic store_output (ssa_1, ssa_0) (4, 15, 0, 160, 130) /* base=4 */ /* wrmask=xyzw */ /* component=0 */ /* src_type=float32 */ /* location=2 slots=1 */ /* gl_FragColor */
/* succs: block_1 */
block block_1:
}
Native code for unnamed fragment shader BLORP-clear (sha1 867c2b9794cc6654dbcf091d8a21323f01e0d409)
SIMD16 shader: 2 instructions. 0 loops. 12 cycles. 0:0 spills:fills, 1 sends, scheduled with mode (null). Promoted 0 constants. Compacted 32 to 32 bytes (0%)
START B0 (12 cycles)
mov(4) g114<1>F g2.3<8,2,4>F { align1 WE_all 1N };
sendc(16) null<1>UW g114<0,1,0>F 0x82031100
render MsgDesc: RT write SIMD16/RepData LastRT Surface = 0 mlen 1 rlen 0 { align1 1H EOT };
END B0

然后通过下面代码加载Vertex数据。
glVertexAttribPointer ( 0, 3, GL_FLOAT, GL_FALSE, 0, vVertices );
glEnableVertexAttribArray ( 0 );

调用下面的函数执行Draw操作。
glDrawArrays ( GL_TRIANGLES, 0, 3 );
这个时候才会把对应shader的NIR代码转换成EU code。注意前面执行glCompileShader的时候只是生成了NIR,没有完成EU code的生成,也就是说编译器的后端到这个时候才开始工作。

先生成fragment shader的EU code,然后把EU code配置到下面的command中。
其中GEN9_3DSTATE_PS有一个分量会指明PS对应的EU Code保存地址。
GEN9_3DSTATE_PS
GEN9_3DSTATE_PS_EXTRA

然后生成vertex shader的EU code,然后把EU code配置到下面的command中。
其中GEN9_3DSTATE_VS有一个分量会指明VS对应的EU Code保存地址。
GEN9_3DSTATE_STREAMOUT
GEN9_3DSTATE_SO_DECL_LIST
GEN9_3DSTATE_VS

然后继续配置如下的command。其中GEN9_3DPRIMITIVE用来说明了需要参加绘制的Vertex数目。
state GEN9_BLEND_STATE_ENTRY
state GEN9_BLEND_STATE_ENTRY
state GEN9_BLEND_STATE_ENTRY
state GEN9_BLEND_STATE_ENTRY
state GEN9_BLEND_STATE_ENTRY
state GEN9_BLEND_STATE_ENTRY
state GEN9_BLEND_STATE_ENTRY
state GEN9_BLEND_STATE_ENTRY
GEN9_3DSTATE_PS_BLEND
state GEN9_BLEND_STATE
GEN9_3DSTATE_SF
GEN9_3DSTATE_RASTER
GEN9_3DSTATE_CLIP
GEN9_3DSTATE_WM
GEN9_3DSTATE_LINE_STIPPLE
GEN9_3DSTATE_VERTEX_ELEMENTS
state GEN9_VERTEX_ELEMENT_STATE
GEN9_3DSTATE_VF_INSTANCING
state GEN9_VERTEX_ELEMENT_STATE
GEN9_3DSTATE_VF_INSTANCING
state GEN9_VERTEX_BUFFER_STATE
GEN9_PIPE_CONTROL
state GEN9_CS_CHICKEN1
GEN9_MI_LOAD_REGISTER_IMM
state GEN9_CC_VIEWPORT
GEN9_3DSTATE_VIEWPORT_STATE_POINTERS_CC
state GEN9_SF_CLIP_VIEWPORT
GEN9_3DSTATE_VIEWPORT_STATE_POINTERS_SF_CLIP
GEN9_3DSTATE_URB_VS
GEN9_3DSTATE_URB_VS
GEN9_3DSTATE_URB_VS
GEN9_3DSTATE_URB_VS
state GEN9_BLEND_STATE
GEN9_3DSTATE_BLEND_STATE_POINTERS
state GEN9_COLOR_CALC_STATE
GEN9_3DSTATE_CC_STATE_POINTERS
GEN9_3DSTATE_CONSTANT_VS
GEN9_3DSTATE_CONSTANT_VS
GEN9_3DSTATE_BINDING_TABLE_POINTERS_VS
GEN9_3DSTATE_BINDING_TABLE_POINTERS_VS
GEN9_3DSTATE_BINDING_TABLE_POINTERS_VS
GEN9_3DSTATE_BINDING_TABLE_POINTERS_VS
GEN9_3DSTATE_BINDING_TABLE_POINTERS_VS
GEN9_3DSTATE_SAMPLER_STATE_POINTERS_VS
GEN9_3DSTATE_SAMPLER_STATE_POINTERS_VS
GEN9_3DSTATE_MULTISAMPLE
GEN9_3DSTATE_SAMPLE_MASK
GEN9_3DSTATE_HS
GEN9_3DSTATE_TE
GEN9_3DSTATE_DS
GEN9_3DSTATE_GS
GEN9_3DSTATE_PS
GEN9_3DSTATE_PS_EXTRA
GEN9_3DSTATE_STREAMOUT
GEN9_3DSTATE_CLIP
GEN9_3DSTATE_SF
GEN9_3DSTATE_WM
GEN9_3DSTATE_SBE
GEN9_3DSTATE_SBE_SWIZ
GEN9_3DSTATE_PS_BLEND
GEN9_3DSTATE_WM_DEPTH_STENCIL
GEN9_3DSTATE_SCISSOR_STATE_POINTERS
GEN9_3DSTATE_CLEAR_PARAMS
GEN9_3DSTATE_POLY_STIPPLE_PATTERN
GEN9_3DSTATE_VF_TOPOLOGY
GENX(3DSTATE_VERTEX_BUFFERS)
GEN9_3DSTATE_VF_SGVS
GEN9_3DSTATE_VF
GEN9_3DSTATE_VF_STATISTICS
GEN9_3DPRIMITIVE

最后执行eglSwapBuffers,把前面生成的command都送到GPU kernel driver中,然后启动GPU硬件完成绘制。
GEN9_PIPE_CONTROL
GEN9_PIPE_CONTROL
GEN9_PIPE_CONTROL

0x4 Command配置的实现

这里以Vulkan driver中的Command配置实现为例说明Mesa中Gen gpu的command是如何配置的。对OpenGL driver来说,这部分实现也是很类似的。

通过xml配置文件来保存Command Streamer的结构体信息,然后在编译的时候通过python代码把xml文件转换成对应的配置头文件。这个过程如下图所示。

其中可以看到Gen的各种Driver实现都依赖于这个配置头文件。另外提到一点就是这些配置的函数都是在不同的Driver中实现的,有不少冗余代码,可以作为一个代码优化方向。

xml的内容是参考下面两个文档(以Gen11为例)来生成的。

Intel® Iris® Plus Graphics and UHD Graphics Open Source
Programmer’s Reference Manual
For the 2019 10th Generation Intel CoreTM Processors based on the
“Ice Lake” Platform
Volume 2a - Command Reference: Instructions (Command Opcodes)

Intel® Iris® Plus Graphics and UHD Graphics Open
Source
Programmer’s Reference Manual
For the 2019 10th Generation Intel CoreTM Processors
based on the “Ice Lake” Platform
Volume 8: Command Stream Programming

下面截取了部分xml内容如下所示。其中包括了PIPE_CONTROL的配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<instruction name="PIPE_CONTROL" bias="2" length="6" engine="render">
<field name="DWord Length" start="0" end="7" type="uint" default="4"/>
<field name="3D Command Sub Opcode" start="16" end="23" type="uint" default="0"/>
<field name="3D Command Opcode" start="24" end="26" type="uint" default="2"/>
<field name="Command SubType" start="27" end="28" type="uint" default="3"/>
<field name="Command Type" start="29" end="31" type="uint" default="3"/>
<field name="Depth Cache Flush Enable" start="32" end="32" type="bool"/>
<field name="Stall At Pixel Scoreboard" start="33" end="33" type="bool"/>
<field name="State Cache Invalidation Enable" start="34" end="34" type="bool"/>
<field name="Constant Cache Invalidation Enable" start="35" end="35" type="bool"/>
<field name="VF Cache Invalidation Enable" start="36" end="36" type="bool"/>
<field name="DC Flush Enable" start="37" end="37" type="bool"/>
<field name="Pipe Control Flush Enable" start="39" end="39" type="bool"/>
<field name="Notify Enable" start="40" end="40" type="bool"/>
<field name="Indirect State Pointers Disable" start="41" end="41" type="bool"/>
<field name="Texture Cache Invalidation Enable" start="42" end="42" type="bool"/>
<field name="Instruction Cache Invalidate Enable" start="43" end="43" type="bool"/>
<field name="Render Target Cache Flush Enable" start="44" end="44" type="bool"/>
<field name="Depth Stall Enable" start="45" end="45" type="bool"/>
<field name="Post Sync Operation" start="46" end="47" type="uint">
<value name="No Write" value="0"/>
<value name="Write Immediate Data" value="1"/>
<value name="Write PS Depth Count" value="2"/>
<value name="Write Timestamp" value="3"/>
</field>
<field name="Generic Media State Clear" start="48" end="48" type="bool"/>
<field name="TLB Invalidate" start="50" end="50" type="bool"/>
<field name="Global Snapshot Count Reset" start="51" end="51" type="bool"/>
<field name="Command Streamer Stall Enable" start="52" end="52" type="bool"/>
<field name="Store Data Index" start="53" end="53" type="uint"/>
<field name="LRI Post Sync Operation" start="55" end="55" type="uint">
<value name="No LRI Operation" value="0"/>
<value name="MMIO Write Immediate Data" value="1"/>
</field>
<field name="Destination Address Type" start="56" end="56" type="uint" prefix="DAT">
<value name="PPGTT" value="0"/>
<value name="GGTT" value="1"/>
</field>
<field name="Flush LLC" start="58" end="58" type="bool"/>
<field name="Address" start="66" end="111" type="address"/>
<field name="Immediate Data" start="128" end="191" type="uint"/>
</instruction>

把xml内容转换后头文件内容如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#define GEN9_PIPE_CONTROL_length 6
#define GEN9_PIPE_CONTROL_length_bias 2
// 定义Command的头信息
#define GEN9_PIPE_CONTROL_header \
.DWordLength = 4, \
._3DCommandSubOpcode = 0, \
._3DCommandOpcode = 2, \
.CommandSubType = 3, \
.CommandType = 3
// 定义Command的结构体信息
struct GEN9_PIPE_CONTROL {
uint32_t DWordLength;
uint32_t _3DCommandSubOpcode;
uint32_t _3DCommandOpcode;
uint32_t CommandSubType;
uint32_t CommandType;
bool DepthCacheFlushEnable;
bool StallAtPixelScoreboard;
bool StateCacheInvalidationEnable;
bool ConstantCacheInvalidationEnable;
bool VFCacheInvalidationEnable;
bool DCFlushEnable;
bool PipeControlFlushEnable;
bool NotifyEnable;
bool IndirectStatePointersDisable;
bool TextureCacheInvalidationEnable;
bool InstructionCacheInvalidateEnable;
bool RenderTargetCacheFlushEnable;
bool DepthStallEnable;
uint32_t PostSyncOperation;
#define NoWrite 0
#define WriteImmediateData 1
#define WritePSDepthCount 2
#define WriteTimestamp 3
bool GenericMediaStateClear;
bool TLBInvalidate;
bool GlobalSnapshotCountReset;
bool CommandStreamerStallEnable;
uint32_t StoreDataIndex;
uint32_t LRIPostSyncOperation;
#define NoLRIOperation 0
#define MMIOWriteImmediateData 1
uint32_t DestinationAddressType;
#define DAT_PPGTT 0
#define DAT_GGTT 1
bool FlushLLC;
__gen_address_type Address;
uint64_t ImmediateData;
};

Command配置的宏定义如下。
该宏定义包括了所有Command的配置。

1
2
3
4
5
6
7
8
9
10
11
12
#define __anv_cmd_header(cmd) cmd ## _header
#define __anv_cmd_pack(cmd) cmd ## _pack
#define anv_batch_emit(batch, cmd, name) \
for (struct cmd name = { __anv_cmd_header(cmd) }, \
*_dst = anv_batch_emit_dwords(batch, __anv_cmd_length(cmd)); \
__builtin_expect(_dst != NULL, 1); \
({ __anv_cmd_pack(cmd)(batch, _dst, &name); \
printf("%s\n", #cmd); \
VG(VALGRIND_CHECK_MEM_IS_DEFINED(_dst, __anv_cmd_length(cmd) * 4)); \
_dst = NULL; \
}))

上面的宏定义是通过实现各个不同Command的pack函数(也是通过前面的genXml自动生成的)来实现不同Command的配置的。

例如下面的函数实现了PIPE_CONTROL Command的结构体信息配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
static inline __attribute__((always_inline)) void
GEN9_PIPE_CONTROL_pack(__attribute__((unused)) __gen_user_data *data,
__attribute__((unused)) void * restrict dst,
__attribute__((unused)) const struct GEN9_PIPE_CONTROL * restrict values)
{
uint32_t * restrict dw = (uint32_t * restrict) dst;
dw[0] =
__gen_uint(values->DWordLength, 0, 7) |
__gen_uint(values->_3DCommandSubOpcode, 16, 23) |
__gen_uint(values->_3DCommandOpcode, 24, 26) |
__gen_uint(values->CommandSubType, 27, 28) |
__gen_uint(values->CommandType, 29, 31);
dw[1] =
__gen_uint(values->DepthCacheFlushEnable, 0, 0) |
__gen_uint(values->StallAtPixelScoreboard, 1, 1) |
__gen_uint(values->StateCacheInvalidationEnable, 2, 2) |
__gen_uint(values->ConstantCacheInvalidationEnable, 3, 3) |
__gen_uint(values->VFCacheInvalidationEnable, 4, 4) |
__gen_uint(values->DCFlushEnable, 5, 5) |
__gen_uint(values->PipeControlFlushEnable, 7, 7) |
__gen_uint(values->NotifyEnable, 8, 8) |
__gen_uint(values->IndirectStatePointersDisable, 9, 9) |
__gen_uint(values->TextureCacheInvalidationEnable, 10, 10) |
__gen_uint(values->InstructionCacheInvalidateEnable, 11, 11) |
__gen_uint(values->RenderTargetCacheFlushEnable, 12, 12) |
__gen_uint(values->DepthStallEnable, 13, 13) |
__gen_uint(values->PostSyncOperation, 14, 15) |
__gen_uint(values->GenericMediaStateClear, 16, 16) |
__gen_uint(values->TLBInvalidate, 18, 18) |
__gen_uint(values->GlobalSnapshotCountReset, 19, 19) |
__gen_uint(values->CommandStreamerStallEnable, 20, 20) |
__gen_uint(values->StoreDataIndex, 21, 21) |
__gen_uint(values->LRIPostSyncOperation, 23, 23) |
__gen_uint(values->DestinationAddressType, 24, 24) |
__gen_uint(values->FlushLLC, 26, 26);
const uint64_t v2_address =
__gen_combine_address(data, &dw[2], values->Address, 0);
dw[2] = v2_address;
dw[3] = v2_address >> 32;
const uint64_t v4 =
__gen_uint(values->ImmediateData, 0, 63);
dw[4] = v4;
dw[5] = v4 >> 32;
}

在Vulkan驱动中通过类似的配置的调用代码来设置具体的Command信息。
最后调用的函数是前面实现的GEN9_PIPE_CONTROL_pack()。

1
2
3
4
5
anv_batch_emit(&cmd_buffer->batch, GENX(PIPE_CONTROL), pc) {
pc.DCFlushEnable = true;
pc.RenderTargetCacheFlushEnable = true;
pc.CommandStreamerStallEnable = true;
}

Graphics buffer总结

发表于 2019-06-01 | 阅读次数

0x1 GPU系统Buffer介绍

本文对GPU系统中的buffer管理进行了总结。
简单说来GPU系统使用到的buffer主要包括两部分,其中一部分是GPU绘制输出的frame buffer, 另外一部分是GPU driver中内部运行所需的各种buffer。
这些buffer在CPU和GPU之间的pipeline如下图所示。
从图中可知,Graphics程序运行的时候首先需要CPU先准备好各种数据,如纹理,顶点数据等。另外很大一块是Command,这个是用来驱动GPU硬件执行图形流水线的指令。另外Shader指的是用来执行GPU可编程pileine(如vertex shader,fragment shader等)的GPU指令。
CPU把数据准备好了以后,驱动GPU硬件根据前面buffer里的数据和指令执行绘制工作,绘制的结果需要输出到一个地方,这个就是frame buffer。当然我们知道frame buffer也是可以被GPU再读进来。图中的Bin List部分指的是在Tile Based Render架构特有的buffer,这个buffer用来存储Tile划分(一般称为Binning阶段)以后每个Tile需要绘制的信息。

0x2 Frame Buffer

下面先来介绍一下Android系统中Graphics Buffer管理模块,后续再介绍一下DRM结构下Buffer分配的流程。
Android系统中Graphics Buffer的架构图如下所示。

Android系统中Graphics框架或应用通过Gralloc模块调用到ION driver,再通过ION driver分配出相应的frame buffer。如上图所示,ION driver中提供多种不同的heap。Carveout heap一般是指系统启动的时候预分配好的物理连续地址空间,缺点是这部分内存属于特定驱动独占式的,不能和其他模块共享,目前已经很少使用了。CMA(contiguous memory allocation) heap分配出来的buffer的物理地址也是连续的,但是它是在系统运行过程中通过内核中CMA框架来动态分配的。system heap分配出来的buffer其物理地址一般不是连续的。这三种类型的buffer适用于不同的硬件类型。对没有MMU的硬件来说,一般需要要求其访问的物理地址空间是连续的,所以这种硬件一般要求分配的buffer是CMA heap。对包含MMU的硬件来说,由于可以通过MMU(也称为IOMMU)来做虚拟地址到物理地址的转换,所以可以不需要保证其分配的buffer的物理地址空间是连续的。

下面来介绍一下DRM架构下buffer分配的流程。
传统的Linux系统如Ubuntu系统采用的是DRM架构图形系统,这个时候通过DRM kernel driver中的GEM模块来分配buffer。GEM是DRM kernel driver的buffer管理模块。其分配出来的buffer一般称为GEM buffer。其buffer分配过程简单说明如下。

为了简化DRM架构下GEM buffer的分配工作,其提供了libgbm模块作为管理kernel driver和图形应用之间的buffer分配的桥梁。
libgbm的功能类似于Android上的gralloc+libion。mesa中包括了libgbm的实现代码。libgbm提供了底层DRM driver中buffer管理的封装。一般包括分配(DRM_IOCTL_MODE_CREATE_DUMB),释放(DRM_IOCTL_MODE_DESTROY_DUMB)和mmap操作(DRM_IOCTL_MODE_MAP_DUMB)等操作。各种采用Linux作为内核的操作系统的HAL模块都提供了类似libgbm功能的buffer管理模块。

下面来简单介绍一下为什么要执行 DRM_IOCTL_MODE_MAP_DUMB? DRM_IOCTL_MODE_MAP_DUMB的输入是一个 gem handle,返回结果是一个 offset,通过 offset 可以知道 mmap 当前要操作的dumb buffer。所以对 drm device 进行 mmap 操作时,其参数offset 并不是真正的内存偏移量,而是一个 gem object 的索引值。通过该索引值,drm 驱动就可以准确确定当前要操作的是哪个 gem对象,然后可以获取到与该 object 相对应的物理 buffer,并对完成真正的 mmap 操作。

libgbm中DRM_IOCTL_MODE_MAP_DUMB的使用如下所示。可以看到和前面的解析是可以对应起来的。

1
2
3
4
5
6
7
8
map_arg.handle = bo->handle;
ret = drmIoctl(bo->base.gbm->fd, DRM_IOCTL_MODE_MAP_DUMB, &map_arg);
if (ret)
return NULL;
bo->map = mmap(0, bo->size, PROT_WRITE,
MAP_SHARED, bo->base.gbm->fd, map_arg.offset);

下面来说一下mesa driver中frame buffer分配过程。
调用eglCreateWindowSurface之类的函数会指明需要创建Suface。这个时候一般不会真正分配buffer,而是会创建类似buffer的占位符的Suface对象。然后等真正需要使用这个buffer的时候才完成分配工作,这里面体现了defer分配的思想。
下面介绍一下真正需要分配buffer的时候buffer是如何分配出来的。
首先通过eglMakeCurrent分配,具体流程如下。这个分配出来的buffer供给后续的eglSwapBuffer使用。

eglSwapBuffer在前面分配好的buffer执行完绘制工作以后,对应的buffer可能送去合成模块继续执行合成动作了。如果后面再调用glClear,需要分配新的buffer,具体流程如下。

在eglMakeCurrent和glClear调用执行buffer分配工作的时候,需要找到一块目标buffer执行clear操作,如果这个时候没有可用的buffer,或者buffer大小不符合要求,则需要重新分配一块新的buffer。

前面看到,不同的Graphics API函数都可能触发buffer的分配,但是每个egl context可以分配buffer的数量是有限制的,一般是2~3个。如果buffer用完了,这个时候可能就需要等待一个空的可用的buffer从其他的pipeline中释放出来。如我们可能在systrace上看到glClear函数占用很长时间,这个时候就会感觉很奇怪,因为glClear按理说不应该占用GPU太长时间,这个时候大概率是在等待一个空的buffer变成可用,并不是在GPU在执行繁重的绘制任务。

0x3 GPU driver中内部使用的buffer

这里介绍的buffer一般是指GPU driver运行过程中内部需要的各种buffer,如常见的texture buffer,vertex buffer,shader buffer, command buffer。这种buffer一般用来保存GPU需要读取的各种数据, 这种buffer的使用一般流程是先通过kernel driver分配出来,然后mmap到CPU端,CPU完成写入以后,GPU开始读取。GPU读取的时候会涉及到GPU MMU的动作,需要完成GPU虚拟地址到GPU物理地址的转换。

Andorid的GPU驱动架构下,其内部使用的buffer管理一般是char kernel driver+闭源User space library的架构来实现。kernel driver通过alloc_pages之类的底层API直接分配buffer,然后mmap到User space给CPU写入,写入完成通过ioctl通知GPU硬件开始使用这些buffer。

下面来简单介绍一下Intel Gen i915 kernel driver中GEM buffer实现。
i915 kernel driver提供了下面三种gem buffer实现。
其中shmem_region_ops采用的是shmem机制来分配内存,shmem是一套ipc,通过相应的ipc系统调用shmget能够以指定key创建一块的共享内存。需要使用这块内存的进程可以通过shmat系统调用来获得它。
stolen buffer指的通过GTT(Graphics translation table)来管理的buffer,这里面类似GTT,PPGTT的概念都是很大的一块技术,这里就不深入介绍了。
下面直接贴出i915 kernel driver中三种gem buffer定义和初始化的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
enum intel_region_id {
INTEL_REGION_SMEM = 0,
INTEL_REGION_LMEM,
INTEL_REGION_STOLEN,
INTEL_REGION_UNKNOWN, /* Should be last */
};
static const struct intel_memory_region_ops shmem_region_ops = {
.init = init_shmem,
.release = release_shmem,
.create_object = create_shmem,
};
static const struct intel_memory_region_ops i915_region_stolen_ops = {
.init = init_stolen,
.release = release_stolen,
.create_object = _i915_gem_object_create_stolen,
};
const struct intel_memory_region_ops intel_region_lmem_ops = {
.init = region_lmem_init,
.release = region_lmem_release,
.create_object = __i915_gem_lmem_object_create,
};
int intel_memory_regions_hw_probe(struct drm_i915_private *i915)
{
int err, i;
for (i = 0; i < ARRAY_SIZE(i915->mm.regions); i++) {
struct intel_memory_region *mem = ERR_PTR(-ENODEV);
u32 type;
if (!HAS_REGION(i915, BIT(i)))
continue;
type = MEMORY_TYPE_FROM_REGION(intel_region_map[i]);
switch (type) {
case INTEL_MEMORY_SYSTEM:
mem = i915_gem_shmem_setup(i915);
break;
case INTEL_MEMORY_STOLEN:
mem = i915_gem_stolen_setup(i915);
break;
case INTEL_MEMORY_LOCAL:
mem = intel_setup_fake_lmem(i915);
break;
}
if (IS_ERR(mem)) {
err = PTR_ERR(mem);
drm_err(&i915->drm,
"Failed to setup region(%d) type=%d\n",
err, type);
goto out_cleanup;
}
mem->id = intel_region_map[i];
mem->type = type;
mem->instance = MEMORY_INSTANCE_FROM_REGION(intel_region_map[i]);
i915->mm.regions[i] = mem;
}
......
}

另外我们知道buffer的分配是很heavy的操作,gpu驱动一般都提供了cache机制来缓存使用过的buffer,也就是说延迟释放这些使用过的buffer。mesa和broadcom gpu驱动中实现了用户态驱动的cache机制,mali早期gpu(mali400/450)在kernel space driver中加了cache机制。从性能的角度的来说,用户态驱动的cache应该更好一些,毕竟少了从用户态到内核态的调用,但是用户态驱动的cache实现复杂一些。

Use AI to speed up AV1 encoder

发表于 2019-04-06 | 阅读次数

0x1 AV1的编码复杂性

我们知道AV1的官方参考实现是libaom,由于AV1的编码复杂度高,如果采用libaom编码器来生成AV1的码流离实时编码还有很大的距离。我们知道传统视频编码器中有宏块的概念,宏块是16x16的亮度块 + 2个 8x8的色度快。从HEVC开始,到现在的AV1进一步引入了partiton的概念,也就是树形编码的概念,也就是说把先规定好最大的编码单元,这个最大的编码单元称为super block,在HEVC一般是64x64,在AV1中为128x128。然后进行四叉树划分,AV1中直至划分成4x4,HEVC中直至划分成8x8。而且这种划分进一步扩展到预测单元和变换单元。据统计,AV1编码中的复杂度80%是因为partiton引入的,所以要是能有一个快速方法来加速partiton的判断的话,AV1的编码速度能大幅提升。

AV1中的partiton划分如下图所示。

0x2 AI加速partiton划分

在libaom目前的实现中,AI加速主要用在intra frame的partition划分优化上。
1
2
3
4
av1_intra_mode_cnn_partition(
&cpi->common, x, bsize, x->quad_tree_idx, &partition_none_allowed,
&partition_horz_allowed, &partition_vert_allowed, &do_rectangular_split,
&do_square_split);

该函数的输入是图像的像素值,可以理解为图像对应的纹理。并且需要把对应亮度/色度值转换成0~1之间的浮点数。如下代码所示,c为亮度或色度分量的index,这里c为0,为亮度分量。max_val为亮度/色度分量的最大值255(假设为8bit yuv)。

1
2
3
for (int i = 0; i < height; ++i)
for (int j = 0; j < width; ++j)
input[i * in_stride + j] = (float)dgd[c][i * stride + j] / max_val;

输出是这几个变量partition_none_allowed,partition_horz_allowed,partition_vert_allowed,do_rectangular_split,do_square_split。用来对后续的partition的划分进行优化,从变量的名称可以看到这些变量会对后续的partition划分进行限制,也就是减少了partition的数目。

推理采用的网络是CNN + DNN的结合。
CNN是5层网络结构,网络定义如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
static const CNN_CONFIG av1_intra_mode_cnn_partition_cnn_config = {
NUM_CNN_LAYERS, // num_layers
0, // is_residue
0, // ext_width
0, // ext_height
0, // strict_bounds
{
{
CNN_LAYER_0_IN_CH, // in_channels
CNN_LAYER_0_WIDTH, // filter_width
CNN_LAYER_0_WIDTH, // filter_height
CNN_LAYER_0_OUT_CH, // out_channels
CNN_LAYER_0_HORZ_STRIDE, // skip_width
CNN_LAYER_0_VERT_STRIDE, // skip_height
0, // maxpool
av1_intra_mode_cnn_partition_cnn_layer_0_kernel, // weights
av1_intra_mode_cnn_partition_cnn_layer_0_bias, // bias
PADDING_VALID, // pad
RELU, // activation
0, // deconvolve
0, // branch
BRANCH_NO_COPY, // branch_copy_type
BRANCH_NOC, // branch_combine_type
NO_BRANCH_CONFIG, // branch_config
NO_BN_PARAMS, // bn_params
-1, // output_num
},
{
CNN_LAYER_1_IN_CH, // in_channels
CNN_LAYER_1_WIDTH, // filter_width
CNN_LAYER_1_WIDTH, // filter_height
CNN_LAYER_1_OUT_CH, // out_channels
CNN_LAYER_1_HORZ_STRIDE, // skip_width
CNN_LAYER_1_VERT_STRIDE, // skip_height
0, // maxpool
av1_intra_mode_cnn_partition_cnn_layer_1_kernel, // weights
av1_intra_mode_cnn_partition_cnn_layer_1_bias, // bias
PADDING_VALID, // pad
RELU, // activation
0, // deconvolve
0, // branch
BRANCH_NO_COPY, // branch_copy_type
BRANCH_NOC, // branch_combine_type
NO_BRANCH_CONFIG, // branch_config
NO_BN_PARAMS, // bn_params
3, // output_num
},
{
CNN_LAYER_2_IN_CH, // in_channels
CNN_LAYER_2_WIDTH, // filter_width
CNN_LAYER_2_WIDTH, // filter_height
CNN_LAYER_2_OUT_CH, // out_channels
CNN_LAYER_2_HORZ_STRIDE, // skip_width
CNN_LAYER_2_VERT_STRIDE, // skip_height
0, // maxpool
av1_intra_mode_cnn_partition_cnn_layer_2_kernel, // weights
av1_intra_mode_cnn_partition_cnn_layer_2_bias, // bias
PADDING_VALID, // pad
RELU, // activation
0, // deconvolve
0, // branch
BRANCH_NO_COPY, // branch_copy_type
BRANCH_NOC, // branch_combine_type
NO_BRANCH_CONFIG, // branch_config
NO_BN_PARAMS, // bn_params
2, // output_num
},
{
CNN_LAYER_3_IN_CH, // in_channels
CNN_LAYER_3_WIDTH, // filter_width
CNN_LAYER_3_WIDTH, // filter_height
CNN_LAYER_3_OUT_CH, // out_channels
CNN_LAYER_3_HORZ_STRIDE, // skip_width
CNN_LAYER_3_VERT_STRIDE, // skip_height
0, // maxpool
av1_intra_mode_cnn_partition_cnn_layer_3_kernel, // weights
av1_intra_mode_cnn_partition_cnn_layer_3_bias, // bias
PADDING_VALID, // pad
RELU, // activation
0, // deconvolve
0, // branch
BRANCH_NO_COPY, // branch_copy_type
BRANCH_NOC, // branch_combine_type
NO_BRANCH_CONFIG, // branch_config
NO_BN_PARAMS, // bn_params
1, // output_num
},
{
CNN_LAYER_4_IN_CH, // in_channels
CNN_LAYER_4_WIDTH, // filter_width
CNN_LAYER_4_WIDTH, // filter_height
CNN_LAYER_4_OUT_CH, // out_channels
CNN_LAYER_4_HORZ_STRIDE, // skip_width
CNN_LAYER_4_VERT_STRIDE, // skip_height
0, // maxpool
av1_intra_mode_cnn_partition_cnn_layer_4_kernel, // weights
av1_intra_mode_cnn_partition_cnn_layer_4_bias, // bias
PADDING_VALID, // pad
RELU, // activation
0, // deconvolve
0, // branch
BRANCH_NO_COPY, // branch_copy_type
BRANCH_NOC, // branch_combine_type
NO_BRANCH_CONFIG, // branch_config
NO_BN_PARAMS, // bn_params
0, // output_num
},
},
};

DNN是两层网络结构,网络定义如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static const NN_CONFIG av1_intra_mode_cnn_partition_branch_0_dnn_config = {
BRANCH_0_NUM_DNN_FEATURES,
BRANCH_0_NUM_LOGITS,
BRANCH_0_NUM_DNN_LAYERS,
{
BRANCH_0_NUM_DNN_LAYER_0_UNITS,
BRANCH_0_NUM_DNN_LAYER_1_UNITS,
},
{
av1_intra_mode_cnn_partition_branch_0_dnn_layer_0_kernel,
av1_intra_mode_cnn_partition_branch_0_dnn_layer_1_kernel,
av1_intra_mode_cnn_partition_branch_0_logits_kernel,
},
{
av1_intra_mode_cnn_partition_branch_0_dnn_layer_0_bias,
av1_intra_mode_cnn_partition_branch_0_dnn_layer_1_bias,
av1_intra_mode_cnn_partition_branch_0_logits_bias,
},
};

0x3 推理模型的训练

以上编码过程中采用的CNN + DNN的网络是经过训练的,在libaom的代码中并没有提供这个网络结构的训练过程代码。参考HEVC partition优化可以大体知道其训练过程。应该是设计好网络以后,通过大量样本数据来训练得到该推理网络的。

Buffer sharing in Weston

发表于 2019-03-30 | 阅读次数

0x1 Weston简介

0x11 X系统

X即X11、X Window System,是用于在类UNIX的操作系统上的位图显示的窗口系统,提供了GUI环境的基本框架,可以在显示设备上绘制、移动窗口,通过鼠标、键盘、触摸屏与用户交互. X Server是X显示服务的一种开源实现,其系统结构如下图所示

0x12 Wayland系统

Wayland是一个显示服务协议,服务端为Wayland Compositor,服务端把把X系统中的X Server和Compositor合二为一,作为类Unix操作系统上更现代、简洁的窗口系统,旨在替换X系统。Weston是Wayland Compositor的参考实现。下面是Wayland官网文档给出的架构简图.

Wayland协议为Client/Server模式,客户端为图形应用程序,发送绘制命令,请求在各自输出缓冲区的显示,服务器为Compositor,控制各个客户端输出缓冲区的合成和显示。

Wayland参考实现Weston包括两层协议。

一个为底层IPC协议。采用Linux Domain Socket实现的IPC,这部分采用手动写的C语言实现。

另一个是上层消息协议。这部分采用libffi来实现,从规定格式的XML文件中自动生成,可以灵活地动态扩展或者用于错误验证。处理客户端和Weston之间的上层交互流程,以实现窗口系统的基本功能。

Wayland参考实现包括两部分,libwayland-client和libwayland-server,其架构简图如下所示。

0x2 Buffer管理

在Weston的实现中,客户端先把内容绘制到一个buffer中,然后weston把多个客户端绘制的buffer通过compositor模块合成在一起。这里面涉及到buffer在各个模块之间的传递,从性能的角度考虑,我们很自然地想到如何避免memory copy的问题。下来我们来看一下如何实现zero memory copy的。

Weston中的buffer用wl_buffer对象来描述。这个buffer需要在客户端和Compositor之间共享。目前有两种buffer的管理模式。

  1. wl_shm
    这种方式是通过共享内存的方式来实现客户端和Compositor之间的共享,通过这种方式分配的内存是物理不连续的,这种方式一般用于采用软件绘制的情况,当buffer在客户端绘制完成以后,Compositor得到通知开始合成的时候,需要通过glTexImage2D()函数把buffer作为纹理上传到GPU中,这样的话性能是会受到影响的,因为纹理上传一般是比较耗时的操作。
  2. wl_drm
    这种方式通过Wayland EGL中的相关机制来保证,客户端通过wayland-egl.h的相关接口EGLSurface来创建GPU的输出buffer,然后客户端开始绘制,绘制完成以后,客户端的GPU输出buffer通过eglCreateImageKHR()接口创建EGLImage,这个EGLImage可以直接作为Compositor的输入纹理来使用,不需要额外的拷贝工作。

    开源的mesa drm实现定义了drm Wayland扩展, 这个时候客户端和Compositor之间可以共享drm (GEM) buffers。

    下面介绍一下基于KMS BO buffer type的mesa wl_drm共享流程





0x3 与Android上图形系统的比较

  1. Buffer的分配
    Android上的buffer是在服务端(SurfaceFlinger)分配的,然后通过ION机制实现buffer的共享。
    而Wayland系统中buffer是在客户端分配的,通过底层EGL驱动提供的buffer共享管理机制,再配合IPC机制实现客户端和服务端之间的buffer共享。

  2. Window管理器的实现
    Android上有单独的WMS(Window Manager Service)模块来做窗口管理的工作。而Wayland系统中在Weston中实现了窗口管理的工作,也就是是窗口管理和合成是在同一个进程中完成的。

  3. Android on Wayland
    把Android上的SurfaceFlinger作为一个Wayland Client,然后参与Weston的合成。架构图如下

    上图的架构有点类似于把X Server作为一个Wayland Client,如下图所示

0x4 Run Weston on Ubuntu

把Wayland protocol和Weston相关实现porting到Ubuntu上,Weston加载X11-backend,也就是说Weston作为X Server的一个Client,把Weston的合成输出在接入到Ubuntu的X Server系统中来输出。

绘制和合成流程如下图所示

测试效果如下图所示

0x5 参考

Wayland architecture
Wayland/Weston Renderer
Android display subsystem as a wayland client

123…5
Kevin Wen

Kevin Wen

45 日志
21 标签
© 2022 Kevin Wen
访问量 访问人数