天天看点

【YOLOv4探讨 之六】Darknet Makefile文件解析

Darknet YOLOv4 Makefile在YOLOv3基础上有调整,针对CPU并行计算,CPU加速都有改进。

上篇文章《【YOLOv4探讨 之五】darknet YOLOv4 编译出现cv::imread(cv::String const&, int)’..未定义的引用》(https://blog.csdn.net/qq_41736617/article/details/118256210)抛出一个问题:原版的makefile和使用cmakelist.txt重新cmake之后的makefile之间是什么对应关系呢?

这里分析后认为原版的makefile是直接使用了NVIDIA的makefile模板,当你把GPU、CUDA、cuDNN配置好了,就可以根据自己的代码略加修改完成makefile编写,需要修改的地方只有OBJ,其他的都是可以直接套用的。而使用cmakelist.txt重新cmake之后的makefile,不关心GPU、CUDA、cuDNN以及加速的配置,所有内容在cmakelist.txt都设置好了,导致生成的makefile差别巨大。

另外,makefile中,将Darknet、CUDA和OpenCV、cuDNN区分设置成DEPS、COMMON、CFLAGS三个变量,当不使用神经网络时,可以将CFLAGS直接裁减掉,便于模块化。

本人将解析注释放在了makefile代码中,如下:

#定义编译使用的变量
# 同时设置 GPU=1 和 CUDNN=1 可以使用 GPU 进行软件运行加速
GPU=1#是否使用cuda库
CUDNN=1#是否使用cuDNN库,cuDNN是用于深度神经网络的GPU加速库。它强调性能、易用性和低内存开销。
CUDNN_HALF=0#按照显卡架构选择架构,进而选择编译使用的库
# 设置 CUDNN_HALF=1 可以进一步完成3倍的显卡加速(张量核心采用混合精度,即非单一使用单精度、双精度,可以根据数据类型进行适配),适用的GPU: Volta, Xavier, Turing and higher
#架构排序(时间顺序):
   #Tesla:市面已经没有相关显卡
   #Fermi:GeForce 400, 500, 600, GT-630
   #Kepler:Tesla K40/K80, GeForce 700, GT-730
   #Maxwell:Tesla/Quadro M series GeForce 900, GTX-970
   #Pascal:Tesla p100,GTX 1080, GTX 1070, GTX 1060
   #Votal:Tesla V100, GTX 1180
   #Turing:T4,GTX 1660 Ti, RTX 2060
   #Ampere:A100

OPENCV=1#是否使用OpenCV库
#通过设置AVX和OPENMP确定是否使用CPU并行加速,(if error occurs then set AVX=0)
AVX=0
OPENMP=0
#OpenMP是一种用于共享内存并行系统的多线程程序设计方案,支持的编程语言包括C、C++和Fortran。OpenMP提
#供了对并行算法的高层抽象描述,特别适合在多核CPU机器上的并行程序设计。编译器根据程序中添加的pragma
#指令,自动将程序并行处理,使用OpenMP降低了并行编程的难度和复杂度。当编译器不支持OpenMP时,程序会退化成普通(串行)程序。程序中已有的OpenMP指令不会影响程序的正常编译运行。


#是否生成libdarknet.so动态链接库
LIBSO=0
#是否使用ZED立体相机,该相机和软件套件能让无人机、机器人和其他机器获得诸如室内/室外防撞、自动导航
#和3D测绘的能力。ZED相机的视觉能力来自于CUDA,也就是Nvidia顶级图形显卡的编程模型。它让运行相机配套
#软件的的计算机有能力以15fps的速度去处理最高4416x1242分辨率的实时景深地图。需要下载专用的SDK,这里
#不讨论。
ZED_CAMERA=0 # ZED SDK 3.0 and above
ZED_CAMERA_v2_8=0 # ZED SDK 2.X

USE_CPP=0#使用g++还是gcc
DEBUG=0#使用debug还是realse

#架构设置,需要对应特定的显卡
#--generate-code <specification>,...             (-gencode)                      
#        This option provides a generalization of the '--gpu-architecture=<arch> --gpu-code=<code>,
#        ...' option combination for specifying nvcc behavior with respect to code
#        generation.  Where use of the previous options generates code for different
#        'real' architectures with the PTX for the same 'virtual' architecture, option
#        '--generate-code' allows multiple PTX generations for different 'virtual'
#        architectures.  In fact, '--gpu-architecture=<arch> --gpu-code=<code>,
#       ...' is equivalent to '--generate-code arch=<arch>,code=<code>,...'.
#        '--generate-code' options may be repeated for different virtual architectures.
#        Allowed keywords for this option:  'arch','code'.


#此选项提供'--gpu架构=<arch>--gpu代码=<code>的泛化,“…”选项组合,用于指定与代码相关的nvcc代码生
#成相关的操作。选项的使用会为不同的虚拟架构相同的真实架构提供并行线程执行(Parallel Thread 
#eXecution,PTX)的代码泛化,此选项允许的关键字为“arch”、“code”。 
#由以上文档注释可知,CUDA库中使用C++模板编成,根据不同的虚拟架构进行泛化,不同的虚拟架构对应各种各
#样真是世界中的显卡架构。

#下面是不同显卡的对应关系
# Tesla V100
# ARCH= -gencode arch=compute_70,code=[sm_70,compute_70]

# GeForce RTX 2080 Ti, RTX 2080, RTX 2070, Quadro RTX 8000, Quadro RTX 6000, Quadro RTX 5000, Tesla T4, XNOR Tensor Cores
# ARCH= -gencode arch=compute_75,code=[sm_75,compute_75]

# Jetson XAVIER
# ARCH= -gencode arch=compute_72,code=[sm_72,compute_72]

# GTX 1080, GTX 1070, GTX 1060, GTX 1050, GTX 1030, Titan Xp, Tesla P40, Tesla P4
# ARCH= -gencode arch=compute_61,code=sm_61 -gencode arch=compute_61,code=compute_61

# GP100/Tesla P100 - DGX-1
# ARCH= -gencode arch=compute_60,code=sm_60

# For Jetson TX1, Tegra X1, DRIVE CX, DRIVE PX - uncomment:
# ARCH= -gencode arch=compute_53,code=[sm_53,compute_53]

# For Jetson Tx2 or Drive-PX2 uncomment:
# ARCH= -gencode arch=compute_62,code=[sm_62,compute_62]

#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_61,code=[sm_61,compute_61]
ARCH= -gencode arch=compute_75,code=[sm_75,compute_75]

#运行uname命令,确定操作系统,$(shell+命令),得到可以引用的参数,UBUNTU下运行得到结果是Linux
#”:=”就表示直接赋值,赋予当前位置的值。
OS := $(shell uname)

#变量赋值,使用”=”进行赋值,变量的值是整个makefile中最后被指定的值。由于变量只赋值一次,不再更
#改,可以这样使用
#使用“=”便于在最后面修改赋值
VPATH=./src/
EXEC=darknet
OBJDIR=./obj/

ifeq ($(LIBSO), 1)
LIBNAMESO=libdarknet.so#待使用的动态链接库,用于拼接链接库路径,一般用不到
APPNAMESO=uselib#指定动态链接库路径
endif

ifeq ($(USE_CPP), 1)
CC=g++
else
CC=gcc#USE_CPP=0,这里使用gcc
endif

CPP=g++ -std=c++11 #使用c++11
NVCC=nvcc #nvidia编译器命令

#--optimize <level>                              (-O)                            
#        Specify optimization level for host code.
#指定主机代码的优化级别
OPTS=-Ofast

#8个不同的-O选项
#    -O ( 与 -O1 相同)
#    -O0 ( 不进行优化,如果没有指定优化级别,则默认设置)
#    -O1 ( 最小优化)
#    -O2 ( 优化更多)
#    -O3 ( 优化更多)
#    -Ofast ( 优化到破坏标准合规性的点)
#    -Og ( 优化调试体验。-Og启用不干扰调试的优化。它应该是标准edit-compile-debug周期的最佳选择级
#别,提供合理的优化级别,同时保持快速编译和良好的调试体验。
#    -Os ( 优化大小。-Os 支持所有通常不增加代码大小的-O2 优化。它还执行了进一步优化以减少代码大
小。
#-Os 禁用以下优化标志:
#-falign-functions -falign-jumps -falign-loops -falign-labels 
#-freorder-blocks -freorder-blocks-and-partition 
#-fprefetch-loop-arrays -ftree-vect-loop-version)

#-Ofast: 等效于 -O3 -ffast-math 
#-ffast-math 触发non-standards-compliant浮点优化。
#这允许编译器假装浮点数是无限精确的,并且它们上的代数遵循实数代数的标准规则。它告诉编译器告诉硬件
#刷新denormals到零,把denormals当作零对待,至少在一些处理器上,包括x86和 x86-64.

#编译选项中指定 -pthread 会附加一个宏定义 -D_REENTRANT,该宏会导致 libc 头文件选择那些thread-safe
#的实现;
#链接选项中指定 -pthread 则同 -lpthread 一样,只表示链接 POSIX thread 库。由于 libc 用于适应 
thread-safe 的宏定义可能变化,
#因此在编译和链接时都使用 -pthread 选项而不是传统的 -lpthread 能够保持向后兼容,并提高命令行的一致
性。

#NVCC:--debug                                         (-g)                            
#        Generate debug information for host code.
#-g选项可以产生带有调试信息的目标代码
ifeq ($(DEBUG), 1)
#OPTS= -O0 -g
#OPTS= -Og -g
COMMON+= -DDEBUG #功能相当于C语言中的#define OPENCV,方法是把DDEBUG添加到编译设置参数COMMON
CFLAGS+= -DDEBUG
else
ifeq ($(AVX), 1)
#如果AVX==1,则-ffp-contract=fast,表示通过使用浮点表达式收缩进行CPU加速
#-mavx -mavx2 -msse3 -msse4.1 -msse4.2 -msse4a都是CPU并行编程的参数,这里不展开
CFLAGS+= -ffp-contract=fast -mavx -mavx2 -msse3 -msse4.1 -msse4.2 -msse4a
endif
endif

CFLAGS+=$(OPTS)#将指定的主机代码的优化级别设置到编译参数中

#Minimal GNU(POSIX)system on Windows,是一个小型的GNU环境,包括基本的bash,make等等。与Cygwin大
#致相当。
ifneq (,$(findstring MSYS_NT,$(OS)))
LDFLAGS+=-lws2_32
endif

#这里最关键,链接OPENCV的库,库的名称在opencv.pc或opencv4.pc中
ifeq ($(OPENCV), 1)
COMMON+= -DOPENCV
CFLAGS+= -DOPENCV
LDFLAGS+= `pkg-config --libs opencv4 2> /dev/null || pkg-config --libs opencv`
COMMON+= `pkg-config --cflags opencv4 2> /dev/null || pkg-config --cflags opencv`
#以上命令也可以改写为:
#LIBOPENCV_INC = $(shell pkg-config --cflags opencv4 2> /dev/null || pkg-config --cflags opencv)
#LIBOPENCV_LIBS = $(shell pkg-config --libs opencv4 2> /dev/null || pkg-config --libs opencv)
#LDFLAGS+= $(LIBOPENCV_LIBS)
#COMMON+= $(LIBOPENCV_INC)
endif

ifeq ($(OPENMP), 1)
CFLAGS+= -fopenmp
LDFLAGS+= -lgomp
endif
#OpenMP是代码与其执行之间的中介.每个#pragma omp语句都转换为对其相应的OpenMP库函数的调用,并且它就是
#它的全部内容.多线程执行(启动线程,连接和同步它们等)始终由操作系统(OS)处理.所有OpenMP都会为我们处理
#这些低级别依赖于操作系统的线程调用.

#-fopenmp标志是一个高级标志,它不仅仅包括GCC的OpenMP实现(gomp).这个gomp库需要更多的库来访问操作系统
#的线程功能.在符合POSIX的操作系统上,OpenMP通常基于pthread,需要链接.它可能还需要实时扩展库(librt)来
#处理某些操作系统,而不是其他操作系统.当使用动态链接时,应该自动发现所有内容,但是当你指定-static时,
#我认为你已经陷入了Jakub Jelinek here所描述的情况.但是现在,当-static时,pthread(和rt,如果需要)应该#自动链接用来.

#除了链接依赖项之外,-fopenmp标志还会激活一些pragma语句处理.您可以在整个GCC代码中看到(如here和
#here),如果没有-fopenmp标志(仅通过链接gomp库不触发),多个pragma将不会转换为相应的OpenMP函数调用.我
#只是尝试了一些示例代码,并且-lgomp和-fopenmp都生成了一个链接相同库的可运行的可执行文件.在我的简单
#示例中唯一的区别是-fopenmp具有-lgomp不具有的符号:GOMP_parallel @@ GOMP_4.0(代码here),该函数初始
#化执行#pragma请求的forks的并行部分在我的示例代码中omp parallel.因此,-lgomp版本没有将pragma转换为
#对GCC的OpenMP实现的调用.两者都产生了可执行的可执行文件,但在这种情况下只有-fopenmp标志产生了并行可
#执行文件.

#GCC需要-fopenmp来处理所有OpenMP pragma.没有它,您的并行部分将不会分叉任何线程,这可能会造成严重破
坏,具体取决于您的内部代码所做的假设.


ifeq ($(GPU), 1)
COMMON+= -DGPU -I/usr/local/cuda/include/#调用cuda库
CFLAGS+= -DGPU
ifeq ($(OS),Darwin) #MAC
LDFLAGS+= -L/usr/local/cuda/lib -lcuda -lcudart -lcublas -lcurand
else
LDFLAGS+= -L/usr/local/cuda/lib64 -lcuda -lcudart -lcublas -lcurand
endif
endif

ifeq ($(CUDNN), 1)
COMMON+= -DCUDNN

ifeq ($(OS),Darwin) #MAC
CFLAGS+= -DCUDNN -I/usr/local/cuda/include
LDFLAGS+= -L/usr/local/cuda/lib -lcudnn
else
CFLAGS+= -DCUDNN -I/usr/local/cudnn/include#调用cudnn库
LDFLAGS+= -L/usr/local/cudnn/lib64 -lcudnn
endif
endif

ifeq ($(CUDNN_HALF), 1)
COMMON+= -DCUDNN_HALF
CFLAGS+= -DCUDNN_HALF
ARCH+= -gencode arch=compute_70,code=[sm_70,compute_70]#关键是根据架构完成代码泛化
endif

#没有安装ZED相机不考虑
ifeq ($(ZED_CAMERA), 1)
CFLAGS+= -DZED_STEREO -I/usr/local/zed/include
ifeq ($(ZED_CAMERA_v2_8), 1)
LDFLAGS+= -L/usr/local/zed/lib -lsl_core -lsl_input -lsl_zed
#-lstdc++ -D_GLIBCXX_USE_CXX11_ABI=0
else
LDFLAGS+= -L/usr/local/zed/lib -lsl_zed
#-lstdc++ -D_GLIBCXX_USE_CXX11_ABI=0
endif
endif

#Darknet将所有的源码放在统一的文件夹/src/中,OBJ对应所有的目标文件
OBJ=image_opencv.o http_stream.o gemm.o utils.o dark_cuda.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 
darknet.o detection_layer.o captcha.o route_layer.o writing.o box.o nightmare.o 
normalization_layer.o avgpool_layer.o coco.o dice.o yolo.o detector.o layer.o compare.o 
classifier.o local_layer.o swag.o shortcut_layer.o activation_layer.o rnn_layer.o gru_layer.o 
rnn.o rnn_vid.o crnn_layer.o demo.o tag.o cifar.o go.o batchnorm_layer.o art.o region_layer.o 
reorg_layer.o reorg_old_layer.o super.o voxel.o tree.o yolo_layer.o gaussian_yolo_layer.o 
upsample_layer.o lstm_layer.o conv_lstm_layer.o scale_channels_layer.o sam_layer.o

ifeq ($(GPU), 1)
LDFLAGS+= -lstdc++#如果包含GPU编译,则涉及C++和C的混合编译,增加-lstdc++
#gcc可以编译c++文件,也可以编译c文件,但默认是编译c文件的,加-lstdc++表示编译c++文件,即链接c++
#库,加-lc表示链接c库,默认情况下就是链接c库,所以如果编译c文件可以不加-lc。

#把OBJ分开写,说明下面的目标函数会涉及到显卡加速,包括计算卷积,计算激活函数(用的太多了,到处都
#用),图像和向量转换,线性代数基础库调用,crop、dropout、maxpool、avgpool层计算,网络前向、反向传
#播等,对应的源文件为.cu文件
OBJ+=convolutional_kernels.o activation_kernels.o im2col_kernels.o col2im_kernels.o blas_kernels.o 
crop_layer_kernels.o dropout_layer_kernels.o maxpool_layer_kernels.o network_kernels.o avgpool_layer_kernels.o
endif

OBJS = $(addprefix $(OBJDIR), $(OBJ))#为目标文件增加路径
#收集所有的头文件
DEPS = $(wildcard src //*.h) Makefile include/darknet.h

#将obj、backup、results、setchmod、darknet、libdarknet.so、uselib都包含到文件编译中,就是你只输入
#make命令时,也处理所有的文件,并不特指具体哪个文件或路径,至声明依赖关系
all: $(OBJDIR) backup results setchmod $(EXEC) $(LIBNAMESO) $(APPNAMESO)


ifeq ($(LIBSO), 1)
#在动态库中生成位置无关的代码。通过全局偏移表(GOT)访问所有常量地址。程序启动时动
#态加载程序解析GOT条目。
CFLAGS+= -fPIC$

#确定动态链接库依赖关系
(LIBNAMESO): $(OBJDIR) $(OBJS) include/yolo_v2_class.hpp src/yolo_v2_class.cpp
	$(CPP) -shared -std=c++11 -fvisibility=hidden -DLIB_EXPORTS $(COMMON) $(CFLAGS) $(OBJS) src/yolo_v2_class.cpp -o [email protected] $(LDFLAGS)

$(APPNAMESO): $(LIBNAMESO) include/yolo_v2_class.hpp src/yolo_console_dll.cpp
	$(CPP) -std=c++11 $(COMMON) $(CFLAGS) -o [email protected] src/yolo_console_dll.cpp $(LDFLAGS) -L ./ -l:$(LIBNAMESO)
endif

#根据目标文件、所有依赖文件生成主程序
#DEPS为darknet源码,COMMON包含依赖的cuda和opencv,CFLAGS包含cudnn,如果换了其他的模型,比如非卷积操
作,可以直接不考虑CFLAGS
$(EXEC): $(OBJS)
	$(CPP) -std=c++11 $(COMMON) $(CFLAGS) $^ -o [email protected] $(LDFLAGS)

#$<指代第一个前置条件:%.c;将COMMON、CFLAGS对应的源文件,进行g++编译、汇编将到目标代码,不进行链
接
$(OBJDIR)%.o: %.c $(DEPS)
	$(CC) $(COMMON) $(CFLAGS) -c $< -o [email protected]

#将所有.cpp源文件、头文件, 将COMMON、CFLAGS对应的源文件,进行g++ -std=c++11编译、汇编到目标代码,
不进行链接
$(OBJDIR)%.o: %.cpp $(DEPS)
	$(CPP) -std=c++11 $(COMMON) $(CFLAGS) -c $< -o [email protected]

#将所有.cu源文件和目标文件,结合泛化的CUDA,和依赖库,进行nvcc编译、汇编将到目标代码,不进行链接
$(OBJDIR)%.o: %.cu $(DEPS)
	$(NVCC) $(ARCH) $(COMMON) --compiler-options "$(CFLAGS)" -c $< -o [email protected]

$(OBJDIR):
	mkdir -p $(OBJDIR) #-p 确保目录名称存在,不存在的就建一个
backup:
	mkdir -p backup
results:
	mkdir -p results
setchmod:
	chmod +x *.sh

.PHONY: clean

clean:
	rm -rf $(OBJS) $(EXEC) $(LIBNAMESO) $(APPNAMESO)
           

完结,撒花!

继续阅读