Darknet 是一个深度学习框架,相对小巧轻量,不涉及计算图,自动微分等概念。darkent 的代码也没有太多抽象,没有 Tensor 的结构, 所有在网络中传输的对象都是用 float *
完成的,所以读懂代码的门槛相对较低。但这也会带来一些问题,框架本身不能提供像 tensorflow 或者 pytorch 那样灵活的前端 API ,由于没有太多抽象,所以每个 layer 的实现都需要对底层的指针操作很熟悉。读代码的目的就是好奇 darknet 是怎么实现的。
先统计一下代码量,总共 3 万行左右, 核心 C 的代码应该 2 万左右,当然代码量不能说明太多问题,Redis 也才一万多行 C 代码。
构建方式
复杂项目的构建系统本身对我而言就是一个门槛,比如 Pytorch , Pytorch 最开始是基于 CPython 写的,所以要知道 CPython 的扩展怎么写,以及 Python 的构建工具怎么用等等。好在 Darkent 几乎没有第三方依赖,如果不实用 Opencv 和 cuda 和 OpenMP 就完全没依赖。拉代码直接编译。
git clone https://github.com/pjreddie/darknet.git
make
然后可以参考官方文档跑下cifar10
src
文件夹下是源码, darknet 下是一个一个 C 的 header 文件。examples
是一些具体任务相关的实现。
├── Makefile
├── README.md
├── backup
├── cfg
├── darknet
├── data
├── examples
├── include
├── python
├── scripts
└── src
Makefile 解读
对 Makefile 做些解释,方便在改动代码后知道如何编译运行。比如我基于 darknet 增加了一个 layer 如何添加到项目里编译
前面 5 行都是编译条件,类似开关一样的东西,0
就是关闭, 1
就是开启,这些条件会在后面的代码里用到,比如 #ifdef GPU
告诉编译器是否编译 GPU 部分的代码。
GPU=0
CUDNN=0
OPENCV=0
OPENMP=0
DEBUG=0
接下来定义了一些全局变量, 我把这写变量分为两类,一类是编译器相关,一类是项目本身相关的,我可能重点关注项目相关的,但还是先来看下编译器相关的,这又分为 GPU 和 CPU 两部分
GPU 相关的编译器是 NVCC
ARCH
是 GPU 编译相关的,我猜是和具体显卡有关的,具体可参考官方文档 CC
是指定的编译器,我想应该是 C Compiler 的意思吧,很多项目都这样用的,LDFLAGS= -lm -pthread
是需要链接的库, -lm
应该是数学库 ,就是在代码里要用的 math.h
-pthread
是线程
CPP=g++
是指定 c++ 编译器NVCC=nvcc
是指定 GPU 代码的编译器AR=ar
和 ARFLAGS=rcs
ar
在这里是用来把多个目标文件打包成静态库的。
CFLAGS=-Wall -Wno-unused-result -Wno-unknown-pragmas -Wfatal-errors -fPIC
就是 C 编译器的一些参数。
ARCH= -gencode arch=compute_30,code=sm_30 \
-gencode arch=compute_35,code=sm_35 \
-gencode arch=compute_50,code=[sm_50,compute_50] \
-gencode arch=compute_52,code=[sm_52,compute_52]
# -gencode arch=compute_20,code=[sm_20,sm_21] \ This one is deprecated?
# This is what I use, uncomment if you know your arch and want to specify
# ARCH= -gencode arch=compute_52,code=compute_52
CC=gcc
CPP=g++
NVCC=nvcc
AR=ar
ARFLAGS=rcs
OPTS=-Ofast
LDFLAGS= -lm -pthread
CFLAGS=-Wall -Wno-unused-result -Wno-unknown-pragmas -Wfatal-errors -fPIC
再来看项目相关的一些变量 , VPATH 是定义了源码的路径是 src
和 example
SLIB
和 ALIB
包的名字EXEC
是最终的可执行文件OBJDIR
缓存目标文件
最后两行在 中间定义的,OBJ
定义目标文件,每个 c 文件都会对应一个目标文件EXECOBJA
是可执行文件,也就是任务相关的代码生产的,比如分类目标检测等等,如果我增加了一个 layer 就在 OBJ
后面添加,如果 增加一个具体任务就在 examples
下些代码,然后在 EXECOBJA
添加对象文件
VPATH=./src/:./examples
SLIB=libdarknet.so
ALIB=libdarknet.a
EXEC=darknet
OBJDIR=./obj
OBJ=gemm.o utils.o cuda.o deconvolutional_layer.o convolutional_layer.o list.o image.o activations.o im2col.o col2im.o blas.o crop_layer.o dropout_layer.o maxpool_layer.o softmax_layer.o data.o matrix.o network.o connected_layer.o cost_layer.o parser.o option_list.o detection_layer.o route_layer.o upsample_layer.o box.o normalization_layer.o avgpool_layer.o layer.o local_layer.o shortcut_layer.o logistic_layer.o activation_layer.o rnn_layer.o gru_layer.o crnn_layer.o demo.o batchnorm_layer.o region_layer.o reorg_layer.o tree.o lstm_layer.o l2norm_layer.o yolo_layer.o iseg_layer.o image_opencv.o
EXECOBJA=captcha.o lsd.o super.o art.o tag.o cifar.o go.o rnn.o segmenter.o regressor.o classifier.o coco.o yolo.o detector.o nightmare.o instance-segmenter.o darknet.o
根据不同条件,然后对一些全集参数会做些修改, 比如 下面的是否开启 OPENCV
ifeq ($(OPENCV), 1)
COMMON+= -DOPENCV
CFLAGS+= -DOPENCV
LDFLAGS+= `pkg-config --libs opencv` -lstdc++
COMMON+= `pkg-config --cflags opencv`
endif
给所有的 目标文件加上前缀,也就是指定文件夹,DEPS 是头文件的依赖
EXECOBJ = $(addprefix $(OBJDIR), $(EXECOBJA))
OBJS = $(addprefix $(OBJDIR), $(OBJ))
DEPS = $(wildcard src/*.h) Makefile include/darknet.h
当我们输入 make
指令的时候会调用 all
命令,冒号后的都是一个指令, 按顺序执行。指令可以理解为一组 shell 脚本
all: obj backup results $(SLIB) $(ALIB) $(EXEC)
比如 obj
就是在创建目标文件的存储文件夹,backup
和 results
也是创建需要的文件夹。
obj:
mkdir -p obj
backup:
mkdir -p backup
results:
mkdir -p results
需要解释下的是下面这组指令
$(EXEC): $(EXECOBJ) $(ALIB)
$(CC) $(COMMON) $(CFLAGS) $^ -o $@ $(LDFLAGS) $(ALIB)
$(ALIB): $(OBJS)
$(AR) $(ARFLAGS) $@ $^
$(SLIB): $(OBJS)
$(CC) $(CFLAGS) -shared $^ -o $@ $(LDFLAGS)
$(OBJDIR)%.o: %.cpp $(DEPS)
$(CPP) $(COMMON) $(CFLAGS) -c $< -o $@
$(OBJDIR)%.o: %.c $(DEPS)
$(CC) $(COMMON) $(CFLAGS) -c $< -o $@
$(OBJDIR)%.o: %.cu $(DEPS)
$(NVCC) $(ARCH) $(COMMON) --compiler-options "$(CFLAGS)" -c $< -o $@
这组指令用了 Makefile 的通配符和特殊变量,$(OBJDIR)%.o:
相当于定义了多个指令 obj/gemm.o
这种,
从输出能看到具体执行的 shell
gcc -Iinclude/ -Isrc/ -Wall -Wno-unused-result -Wno-unknown-pragmas -Wfatal-errors -fPIC -Ofast -c ./src/gemm.c -o obj/gemm.o
gcc -Iinclude/ -Isrc/ -Wall -Wno-unused-result -Wno-unknown-pragmas -Wfatal-errors -fPIC -Ofast -c ./src/utils.c -o obj/utils.o
gcc -Iinclude/ -Isrc/ -Wall -Wno-unused-result -Wno-unknown-pragmas -Wfatal-errors -fPIC -Ofast -c ./src/cuda.c -o obj/cuda.o