分离编译并打包 CUDA 函数库

最近遇到一个小问题:我写了一个小的库,这个库需要同时提供 CUDA device API 和 CPU host C API。本文记录一下编译、打包和链接到这个库的方法。

CUDA 分离编译 (separate compilation) 允许跨文件访问 device functions and variables. CUDA 文档给出了这么一个流程图:

主要有两步:

  1. 对于需要将本文件的 device functions and variables 暴露给其他文件访问的 CUDA source code,nvcc 需要加上 -rdc=true/--device-c 参数来使用分离编译;
  2. 分离编译得到的 obj files 需要再用 device linker 链接一个新的 obj file,然后把所有 obj files 一起用 host linker 处理得到 executable / library。

Tricky 的地方出在图上语焉不详处,即最后一步 host linker 到 library。我摸索了一下以后发现 device linker 这一步不应该放在打包 library 中,而应该放在最后生成可执行文件的过程中。同时,需要打开 -fPIC 编译选项。下面给出两个我测试过的 makefiles,分别用来编译并打包库,以及编译应用程序和链接到我们打包的库。

假设我们的库和应用程序:

  • 只有 CUDA code 和 C code,C code 使用 C99 标准
  • CUDA code 使用 CUDA 10.0 来编译,CUDA 10.0 安装在 /usr/local/cuda-10.0
  • CUDA code 只为 Pascal 和 Volta 架构生成代码
  • C code 包含 MPI 和 OpenMP 函数,需要使用 mpicc (或者其他 MPI compiler wrapper)来编译
  • 我们的库打包好以后安装在 $LIBDIR/lib/libmylib.a, 头文件安装在 $LIBDIR/include/ , 应用程序可执行文件名为 myapp.exe

下面这个 makefile 适用于打包 library:

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
LIBA    = libmylib.a

CC      = mpicc
CFLAGS  = -O3 -Wall -g -std=c99 -fPIC

GENCODE_SM60  = -gencode arch=compute_60,code=sm_60
GENCODE_SM70  = -gencode arch=compute_70,code=sm_70
GENCODE_FLAGS = $(GENCODE_SM60) $(GENCODE_SM70)

CUDA_PATH   ?= /usr/local/cuda-10.0
NVCC        = $(CUDA_PATH)/bin/nvcc
NVCCFLAGS   = -O3 -g --compiler-options -fPIC $(GENCODE_FLAGS)

ifeq ($(shell $(CC) --version 2>&1 | grep -c "icc"), 1)
AR      = xiar rcs
CFLAGS += -fopenmp -xHost
endif

ifeq ($(shell $(CC) --version 2>&1 | grep -c "gcc"), 1)
AR      = ar rcs
CFLAGS += -fopenmp -lm -march=native -Wno-unused-result -Wno-unused-function
endif

C_SRCS  = $(wildcard *.c)
C_OBJS  = $(C_SRCS:.c=.c.o)
CU_SRCS = $(wildcard *.cu)
CU_OBJS = $(CU_SRCS:.cu=.cu.o)
OBJS    = $(C_OBJS) $(CU_OBJS) 

# Delete the default old-fashion double-suffix rules
.SUFFIXES:

all: $(LIBA)

$(LIBA): $(OBJS) 
	$(NVCC) $(NVCCFLAGS) -lib -o $@ $^

%.c.o: %.c
	$(CC) $(CFLAGS) -o $@ -c $^

%.cu.o: %.cu
	$(NVCC) $(NVCCFLAGS) -rdc=true -o $@ -c $^ 

clean:
	rm $(OBJS) $(LIBA)

下面这个 makefile 适用于编译应用程序并链接到我们打包好的库:

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
EXE     = myapp.exe

CC      = mpicc
CFLAGS  = -O3 -Wall -g -std=c99 -fPIC -I$(LIBDIR)/include
LDFLAGS = -fopenmp -L$(CUDA_PATH)/lib64 -L$(LIBDIR)/lib -L./ -lcuda -lcudart -lmylib

GENCODE_SM60  = -gencode arch=compute_60,code=sm_60
GENCODE_SM70  = -gencode arch=compute_70,code=sm_70
GENCODE_FLAGS = $(GENCODE_SM60) $(GENCODE_SM70)

CUDA_PATH   ?= /usr/local/cuda-10.0
NVCC        = $(CUDA_PATH)/bin/nvcc
NVCCFLAGS   = -O3 -g --compiler-options '-fPIC' -I$(LIBDIR)/include $(GENCODE_FLAGS)

ifeq ($(shell $(CC) --version 2>&1 | grep -c "icc"), 1)
CFLAGS += -fopenmp -xHost
endif

ifeq ($(shell $(CC) --version 2>&1 | grep -c "gcc"), 1)
CFLAGS += -fopenmp -lm -march=native -Wno-unused-result -Wno-unused-function
endif

C_SRCS  = $(wildcard *.c)
C_OBJS  = $(C_SRCS:.c=.c.o)
CU_SRCS = $(wildcard *.cu)
CU_OBJS = $(CU_SRCS:.cu=.cu.o)
OBJS    = $(C_OBJS) $(CU_OBJS) dlink.o

# Delete the default old-fashion double-suffix rules
.SUFFIXES:

all: $(EXE)

<your_application>.exe: dlink.o $(OBJS) $(LIB_DIR)/lib/libmylib.a
	$(CC) $^ -o $@ $(LDFLAGS)

%.c.o: %.c
	$(CC) $(CFLAGS) -o $@ -c $^

%.cu.o: %.cu
	$(NVCC) $(NVCCFLAGS) -rdc=true -o $@ -c $^ 

dlink.o: $(CU_OBJS)
	$(NVCC) $(NVCCFLAGS) -dlink -o $@ $(CU_OBJS) $(LIB_DIR)/lib/libmylib.a

clean:
	rm $(OBJS) $(EXE)

注意:生成 myapp.exe 的命令里,这些 obj files 的顺序非常重要!

如果 myapp.exe 只需要一个 myapp.cu,MPI 安装在 MPI_PATH 目录下,那么可以用一个简化版的 makefile 来生成 myapp.exe, 其中直接使用 nvcc 来链接生成最终的可执行文件,跳过 device linker:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
EXE = myapp.exe
CXX = mpicxx

GENCODE_SM60  = -gencode arch=compute_60,code=sm_60
GENCODE_SM70  = -gencode arch=compute_70,code=sm_70
GENCODE_FLAGS = $(GENCODE_SM60) $(GENCODE_SM70)

CUDA_PATH   ?= /usr/local/cuda-10.0
NVCC        = $(CUDA_PATH)/bin/nvcc

NVCC_FLAGS    = -rdc=true -Xcompiler -fopenmp -lineinfo $(GENCODE_FLAGS) -std=c++11 -g
NVCC_FLAGS   += -I$(LIBDIR)/include -I$(MPI_PATH)/include 
NVCC_LDFLAGS  = -ccbin=$(CXX) --compiler-options -fopenmp -L$(CUDA_PATH)/lib64 -lcuda -lcudart
NVCC_LDFLAGS += -L$(MPI_PATH)/lib -lmpi -L$(LIBDIR)/lib -lmylib 

%.exe: %.cu
	$(NVCC) $(NVCC_FLAGS) -c $^ -o $@.o
	$(NVCC) $(GENCODE_FLAGS) $@.o -o $@ $(NVCC_LDFLAGS)
	
clean:
	rm *.o *.exe

Reference

  1. CUDA Toolkit Documentation - Using Separate Compilation in CUDA
  2. Separate Compilation and Linking of CUDA C++ Device Code