Kaleidoscope LLVM-IR codegen

学习内容

完成对 https://llvm.org/docs/tutorial/MyFirstLanguageFrontend/LangImpl03.html 的学习。

练习

看懂原网页后,实现同样的功能,补充测试。
实现时需要做如下改进:
将codegen过程从AST结构中剥离。编写一个codegenerator类,llvm-ir codegen作为该类的子类实现原示例的功能。为后续提供codegen到其他IR的扩展留下空间。

扩展

1 clang 是如何实现AST到LLVM-IR的codegen
2 玩具实现和clang的实现之间有没有实质性的区别
3 是否能以较低的代价实现向方舟编译器的Maple-IR输出

进展

实现了原示例的全部功能。
考虑到单元测试效率较低,暂时尚未实现。后续可以考虑接入LLVM的测试框架。

实现

使用了code_generator这个模板虚基类来表达了处理Kaleidoscope玩具语言AST所需的功能接口。全局的入口只有function和prototype,理论上其实提供两个对应的gen函数就可以了。但是从完整性上考虑,其余的AST类型处理也最好做出约束,所以也一并写到了父类中。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <typename VAL_PTR>
class code_generator
{
public:
virtual ~code_generator(){};
virtual bool gen_function(const function_ast* func) = 0;
virtual bool gen_prototype(const prototype_ast* proto) = 0;
virtual VAL_PTR build_expr(const expr_ast* expr) = 0;
virtual VAL_PTR build_call(const call_ast* callee) = 0;
virtual VAL_PTR build_number(const number_ast* num) = 0;
virtual VAL_PTR build_variable(const variable_ast* var) = 0;
virtual VAL_PTR build_binary_op(const binary_operator_ast* var) = 0;
virtual bool codegen(const ast_vector_t& global_vec) ; //唯一的数据输入,使用AST的vector
virtual void print_IR() = 0; //唯一的数据输出,直接把生成的IR打印到stdin
};

针对LLVM-IR,参考原示例实现了基本相同的IR生成步骤。
主要的差异点包括:
1 去掉了JIT特性所需的全局expr处理;考虑学习方向是传统编译器架构,这个JIT特性会干扰到后续支持全局变量等特性,所以去掉。
2 将原实现基于AST的codegen转换为了独立的codegen框架,后续实现新的codegen会更容易(但是考虑到工作量和复杂度,没有实现vistor模式,两种codegen中一定会出现重复的AST访问模式。这样的模式确实是由AST来固化更好,参考clang对比中的相关内容)。

clang对比

1 clang 是如何实现AST到LLVM-IR的codegen
clang在lib/CodeGen实现了相关的功能。
在实现codegen功能上,代码流程和玩具实现没有本质区别。
都是遍历自己的AST,在这个过程中调用LLVM提供的核心类Module、Function、Builder来生成LLVM的IR。

2 玩具实现和clang的实现之间有没有实质性的区别
从调用LLVM的接口这个角度看没有实质性区别。
但是clang大量实现了Vistor模式,如AST/StmtVisitor.h、AST/RecursiveASTVisitor.h等。这些Visitor很好地将AST的固有访问模式固化下来,这样不同的任务(如print、codegen和evaluate等)就可以只提供针对具体AST结构的回调实现,而复用访问AST的骨架代码。
例如当前这个codegen的任务,它对AST的访问模式就是针对每一个函数递归的访问了所有AST节点。只要对每一种AST提供对应的visit代码就可以了。
例如function的visit就可以写成(如下模板),LLVM-IR生成器(作为下面的subclass)和MAPLE-IR生成器各自提供visit_prototype等visit接口就可以了。

1
2
3
4
check_muldef();
subclass::visit_prototype(func->getproto());
prepare_arg_map();
subclass::visit_func_body(func->get_body());

方舟IR输出

方舟的实现也提供了类似的build函数,可参考mir_builder.h。
应该能以相似的方式(以及成本)从AST直接build出MAPLE-IR。
具体还待确认和核实。

Kaleidoscope ast

学习内容

完成对https://github.com/zy445566/llvm-guide-zh/tree/master/Chapter02

https://llvm.org/docs/tutorial/MyFirstLanguageFrontend/LangImpl02.html
的学习。

练习

看懂原网页后,实现同样的功能,补充测试。
实现时需要做如下改进:
1 作为纯粹的前端,至少在生成LLVM-IR前不应该依赖llvm的任何代码。将所有的llvm::make_unique 改为 std::make_unique(C++11中没有这个接口所以llvm自己实现了,C++14已经有了;最新的LLVM代码也已经完成了向std的切换)
2 将parser作为class封装起来
3 日志输出的封装
4 基本功能测试

扩展

clang 是如何实现AST的构建
玩具实现和clang的实现之间有没有实质性的区别

进展

完成了示例parser的实现,并添加了对应的测试用例

实现

除了完成了原示例外和前面计划的改进外,实现过程中还新增了一些改进,包括:
1 为每一个ast添加了type类型,便于管理ast指针
2 将每一个全局ast存入了统一的vector中,便于后续读取处理
3 为各个ast的内部接口添加了访问接口,以便读出数据进行测试
4 为所有的prototype提供了全局hash表,为后续call的一致性检查提供了便利
5 改进了原示例对toplevel expr的处理逻辑,将这些expr推入2步提供的vector中管理,不再放入一个匿名函数中(以避免重名等问题)
6 严格分层,parser不直接读取原始的char,其全部输入都来自lexer给出的token
7 小幅改进binary_op的相关解析流程,将原有流程中 op优先级和binary表达式结束 混合判断的情况做了拆分,逻辑上更易理解

clang对比

实现后,查看了clang的代码。
clang的parser基本解析流程和玩具示例的没有实质上的区别(注意到clang的parser也是处理token,不直接读取char),尤其是binary_op的解析。
clang的lib/Parse/ParseExpr.cpp的ParseRHSOfBinaryExpression和示例中的parse_binary_expr基本逻辑是完全一样的。
阅读clang代码,发现了一些clang实现可借鉴的地方:
1 使用 using定义专有类型,可以使得代码更简洁

1
using ExprResult = ActionResult<Expr *>;

2 使用class内的模板函数自动完成指针类型的转换,可以使代码更简洁

1
2
template <typename PtrT> PtrT getPtrAs() const {
return get();

3 定制测试框架设计(dump出文本+模式匹配),测试用例开发效率确实远高于直接使用gtest等通用单元测试框架

python深度学习

环境问题总结

启动keras示例,python3 examples/mnist_cnn.py

找不到cuda相关的库。原因是tensorflow不支持10.2版本的cuda,安装10.0版本的cuda并添加库的搜索目录后解决(修改ld.conf后运行ldconfig)。
NVIDIA的驱动仍然可以使用最新的版本。

启动keras示例,python3 examples/mnist_cnn.py

遇到显卡无法正确使用的问题(无法分配内存),可以在代码的头部使用添加如下片段解决。

1
2
3
4
5
6
from tensorflow.compat.v1 import ConfigProto
from tensorflow.compat.v1 import InteractiveSession

config = ConfigProto()
config.gpu_options.allow_growth = True
session = InteractiveSession(config=config)

遇到tensorflow报错Unknown: Failed to get convolution algorithm. This is probably because cuDNN failed to initialize

重启后解决,可能和休眠的处理不好有关系。
使用notebook的时候睡眠,遇到了两次休眠后无法唤醒的问题。

实验问题

5.3.1无法复现书中的96%精度

问题现象

使用书中附带的notebook,在执行快速特征提取时,可以获得预期的90%精度。
但是使用数据增强后的程序,仍然也只能达到90%的精度。

问题分析

反复核对了代码,数据集,然后重新执行了多次仍然只有90%的精度。
使用goolge搜索using-a-pretrained-convnet,可以找到https://github.com/fchollet/deep-learning-with-python-notebooks/issues/21。
阅读其中的讨论,最终可以在https://forums.manning.com/posts/list/42880.page 中找到较为完整的解释。
When I run the code given in the book, my validation accuracy plateaus around 0.90, exactly like the original poster described. I think the problem is that in the book’s code, the images are being read in from disk as numpy arrays of float32 values in the range [0.0, 1.0], due to the keyword argument [tt]rescale=1./255[/tt] in the ImageDataGenerator constructors. These images are then automatically fed directly to the VGG16 convolutional base when the model’s [tt]fit_generator[/tt] method is called.

However, the original VGG16 network was trained on images that were preprocessed by zero-centering each color channel with respect to the ImageNet database. In Keras, there is a function available (in keras.applications.vgg16) that does this transformation, called [tt]preprocess_input[/tt]. In fact, if you test the full pretrained VGG16 network by itself, you will find that in order to get accurate classification results, you must call [tt]preprocess_input[/tt] first before calling the [tt]predict[/tt] method. Furthermore, [tt]preprocess_input[/tt] must be called on images of float32 values in the range [0., 255.], not [0., 1.].

So to summarize, there are two problems with the book’s example: first, it uses images with values in the range [0., 1.], and second, it does not call [tt]preprocess_input[/tt] before feeding the images to the VGG16 base.
问题的本质是本书作者在这一节犯了一个低级错误,在使用VGG16这个预训练模型时,再次训练时送入的数据没有按照VGG16模型的要求进行预处理。同时,作者使用的keras老版本有一个bug,conv_base.trainable = False这句没有生效,所以作者实际上是对整个VGG16模型做了再次完整的训练,网络自动适配了新的值域,精度仍然达到了96%。

按照上面的建议修改notebook中的代码如下,确实能获得预期的96%精度。
使用 preprocessing_function=keras.applications.vgg16.preprocess_input和preprocessing_function=preprocess_input 没有实质性的差异。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from keras.applications.imagenet_utils import preprocess_input

from keras import optimizers

train_datagen = ImageDataGenerator(
#rescale=1./255,
# preprocessing_function=preprocess_input,
preprocessing_function=keras.applications.vgg16.preprocess_input,
rotation_range=40,
width_shift_range=0.2,
height_shift_range=0.2,
shear_range=0.2,
zoom_range=0.2,
horizontal_flip=True,
fill_mode='nearest'
)

#test_datagen = ImageDataGenerator(rescale=1./255)
test_datagen = ImageDataGenerator(preprocessing_function=preprocess_input)

并且,按照后续讨论的结果,对使用快速特征提取的代码也做正确的预处理,确实也能很快达到96%的精度。后面网友的讨论应该是正确的,对于这个问题来说,数据增强并不是必要的。

1
2
3
4
from keras.applications.imagenet_utils import preprocess_input

#datagen = ImageDataGenerator(rescale=1./255)
datagen = ImageDataGenerator(preprocessing_function=preprocess_input)

收获

输入数据的准备极端重要(类型正确,数值范围正确,统一的预处理方法),可能严重影响模型的精度;
深度网络自动学习特征表示的能力确实很强,一个使用0255输入值域预训练的网络,只要不冻结它的weight,即使使用01值域的输入样本重新训练,也仍然能保留其绝大部分的分类能力。网络自动适应了输入值域的失配问题
深度学习确实是实践重于理论,大神也可能犯低级错误。本文作者就犯了输入数据准备的错误。由于低版本keras的bug(froenzen不生效,实际上预训练的网络被完全重新训练)加上深度网络的强大自动学习能力,作者使用低版本的keras即使输入了错误的值域数据也仍然获得了96%的高精度。由于这一结果符合预期,作者忽略了这个错误。

第6章学习的问题记录

6.1.3 冻结与不冻结词嵌入层的对比
作为练习,6.1.3节中提到冻结词嵌入层和不冻结词嵌入层,针对不同的训练样本数量进行性能比较。
实际操作时,因为操作错误没有对训练过的模型进行清零。多次训练时,发现冻结词嵌入层时,如果扩大过训练样本数量,然后再降低样本数量再训练,仍然能保持很高的精度。本次练习中,10000个样本,如果用数千个样本训练过,再用500个样本训练网络,仍然可以保持很高的精度95%。而不冻结词嵌入层,用小样本量再训练,无法维持高精度。应该是反向传播破坏了嵌入层的正确表示。

LSTM训练速度问题
LSTM训练速度很慢,并且GPU的利用率只能到35%。后续应该看看这里的性能瓶颈。

Kaleidoscope lexer

学习内容

完成对https://github.com/zy445566/llvm-guide-zh/blob/master/Chapter01/README.md
https://llvm.org/docs/tutorial/MyFirstLanguageFrontend/LangImpl01.html
的学习。

练习

看懂原网页后,实现同样的功能。
实现时需要做如下改进:
使用class将lexer封装起来,不使用全局变量
使用stream来抽象输入,使得lexer能支持stdin和文件两种方式作为输入。

扩展

clang 是如何实现lexer,如何取出和记录token的
玩具实现和clang的实现之间有没有实质性的区别

进展

实现

参考原有实现,做了如下一些改进:
使用了lexer 类来封装原示例中的全局数据;
使用基于istream的抽象,使得lexer可以支持文件和stdin两种输入;
使用独立的token 类,逻辑更清晰(我们使用string 存放token对应的字符串比较低效,clang是直接使用一个char *ptr+ size来给出这个信息,避免了存放冗余的信息 ,占用内存少速度也更快)。

clang对比

总体来说,clang的lexer核心逻辑和我们的学习示例一致。
lexer部分给出的token,最关键的就是Kind(相当于我们的type)和 PtrData (相当于我们的raw_str)
include/clang/Lex 中可以作为分析入口。
lexer.h中含有各类主要问题的分析入手点。其中FormTokenWithChars函数比较典型。

由于clang需要进行考虑预处理(包括include、marcro等),记录token在源代码中的位置等,其lexer的实现要复杂很多。