Mise en place d'un projet CMake CUDA/C++

Thomas Gredin

Dans certains cas, il peut être intéressant d’exploiter sa GPU pour accélérer certains calculs. CUDA est un langage qui offre la possibilité de réaliser des calculs sur GPU à la place de la CPU et ainsi de tirer parti du grand nombre de processeurs disponibles. Par exemple les applications de traitement d’image/vidéo peuvent en tirer bon bénéfice tout comme les applications de calculs scientifiques.

Comment mettre en place un projet à l’aide de CMake permettant d’exploiter une bibliothèque CUDA « maison » dans un programme C++ ?

Commentez

- Titre : Mise en place d'un projet CMake CUDA/C++

- Auteur : Thomas Gredin

- Parution : 7 juillet 2021

- Public visé :

- Licence : Le contenu de cet article est rédigé par Thomas Gredin et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Pas de Modification 3.0 non transposé. Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright ® 2013 Developpez.com.

Sommaire

- I. Introduction

- I-A. Éléments à prendre en considération

- I-B. Les outils nécessaires

- I-B-1. Installation

- I-B-2. Configuration des outils Visual C++

- II. Mise en place du projet CMake

- II-A. Mise en place de la structure du projet

- II-A-1. Arborescence globale

- II-A-2. Projet principal (C++)

- II-A-3. Projet de la bibliothèque CUDA

- II-B. Compilation du projet

- III. Déclaration et interfaçage des fonctions CUDA

- IV. Remerciements

I - Introduction

Avant de commencer la mise en place du projet, il est important d’apporter des précisions sur certains éléments. Les outils nécessaires et la mise en place de l’environnement de développement seront également abordés.

I-A - Éléments à prendre en considération

Pour produire la bibliothèque CUDA, c’est le compilateur nvcc qui sera utilisé. Il s’agit du compilateur CUDA de NVIDIA. Le but sera de produire une bibliothèque statique. L’édition de liens sera faite avec le programme C++ final lors de sa compilation.

Il est important de noter qu’une contrainte est apportée par le compilateur nvcc qui ne supporte plus la compilation pour les architectures 32 bits. Il faut donc compiler la bibliothèque et le programme C++ pour des architectures 64 bits.

Pour la plateforme Windows, le support de la compilation 32 bits n’est plus possible qu’avec la version 2013 de Visual C++. Pour Linux, le support a été totalement interrompu.

Dans la suite de ce tutoriel, il n’y aura pas de code compliqué, juste des exemples simples pour illustrer la mise en place d’une interopérabilité entre CUDA et C++. Aucune connaissance avancée des langages présents n’est requise, les éléments importants sont présentés en détail pour faciliter la compréhension.

Il semble également important de préciser que ce tutoriel est écrit en priorité pour la plateforme Windows. Cependant, la version basique de projet présentée ici devrait également fonctionner sur les plateformes Linux et macOS.

I-B - Les outils nécessaires

I-B-1 - Installation

Pour la création du projet et la compilation, il faut installer quelques outils :

- le compilateur CUDA de NVIDIA ;

- les outils de compilation Visual C++ ;

- Cmake.

CMake n’a pas toujours inclus le support du langage CUDA. Il faut donc être attentif à la version installée sur votre machine. Celle-ci doit être au minimum la 3.9 !

Sous Windows, l’invite de commande par défaut n’est vraiment pas pratique, ce n’est un secret pour personne. Dans ce tutoriel, toutes les étapes de génération et de compilation sont réalisées en ligne de commande. Il est possible d’utiliser un émulateur de terminal bien plus convivial, je conseille Cmder!

Une fois tous les outils installés, il faut vérifier qu’ils sont bien accessibles dans l’invite de commande. Dans le cas du compilateur CUDA et de CMake, les programmes d’installation se chargent normalement de tout mettre en place (renseignement de la variable PATH sous Windows).

En ce qui concerne les outils de compilation Visual C++, deux possibilités s’offrent à vous :

1. Utiliser l’invite de commande qui vient avec l’installation de l’outil et qui contient déjà les bons chemins vers les exécutables ;

2. Utiliser une invite de commande différente, dans ce cas une configuration est nécessaire

Si vous souhaitez utiliser la première solution, alors vous pouvez passer la partie I.B.2 qui suit pour vous rendre directement à la partie I.B.3.

I-B-2 - Configuration des outils Visual C++

Lors de l’installation des outils de compilation Visual C++, une console de développeur avec les accès aux outils est créée (solution 1). Cependant, il est plus pratique d’y avoir accès dans notre terminal par défaut (solution 2).

Cette configuration peut devenir problématique dans le cas où l’on utilise plusieurs versions du compilateur Visual C++. Dans une telle situation, il peut donc être préférable d’utiliser la première solution proposée.

Pour ce faire, des scripts de configuration de variables sont mis à disposition par cet outil dans le but de mettre en place un environnement de développement. Ils se trouvent dans le dossier \Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build. Le script qui nous intéresse ici est vcvarsall.bat.

Si vous avez décidé d’utiliser l’émulateur de terminal Cmder, sachez qu’il est possible d’exécuter des scripts Bash. De plus, il est possible de créer des alias et de modifier le script de démarrage (à la manière d’un fichier .bashrc sous linux).

Cette fonctionnalité permet de mettre en place l’environnement de développement C++ simplement et automatiquement lors du démarrage du terminal ou alors lors de l’appel d’un alias.

Dans notre cas, nous voulons avoir accès aux outils pour une compilation vers une plateforme cible 64 bits. Il faut donc invoquer la commande suivante :

"C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvarsall.bat" x64

Pour tester, il suffit d’invoquer le compilateur (cl.exe) dans le terminal. Si la commande est reconnue, alors l’environnement est opérationnel.

Il est maintenant temps de commencer la mise en place du projet CMake.

II - Mise en place du projet CMake

Le projet à mettre en place doit satisfaire la condition suivante : un programme C++ doit être en mesure d’utiliser une bibliothèque CUDA dans le but d’accélérer ses opérations de calculs lourds. Pour parvenir à cela, nous allons mettre en place un projet basique composé de deux éléments :

- la bibliothèque contenant le code CUDA et qui devra être exploitable par un programme C++ (une solution permettant cela sera étudiée par la suite) ;

- un programme C++ qui utilisera la bibliothèque.

Ceci se ressent au niveau de la structure du projet, c’est-à-dire le découpage en dossiers et sous-dossiers. CMake permet de mettre en place des sous-projets au sein d’un projet principal. Cette fonctionnalité permet de compiler une bibliothèque qui sera liée à notre exécutable lors de l’édition des liens.

Dans cet article, nous ne verrons pas comment utiliser l’interface graphique qui vient avec l’installation de CMake, tout sera fait en ligne de commande. Si l’utilisation de celle-ci vous intéresse je vous conseille le tutoriel d’Alexandre Laurent portant sur le sujet.

Ces éléments pris en considération, il est temps de passer à la mise en place du projet.

II-A - Mise en place de la structure du projet

II-A-1 - Arborescence globale

Pour commencer, il faut créer le dossier qui contiendra le projet principal, ici il se nommera mainProject. Ce dossier contiendra les sources de l’exécutable à produire.

Pour le sous-projet de la bibliothèque CUDA, il est nécessaire de créer un dossier à la racine du dossier du projet principal. Il se nommera cudaLibProject.

La structure finale obtenue est la suivante :

- mainProject

- cudaLibProject

Il est possible de créer autant de sous-projets que souhaité.

Chacun des dossiers possédera son propre fichier CMakeLists.txt. Ce fichier permet de renseigner les règles de compilation qui mèneront à la génération des fichiers projet.

Les fichiers générés par défaut varient selon la plateforme :

- sous Linux et macOS un Makefile ;

- sous Windows une solution Visual Studio.

Ces fichiers projet permettent par la suite la compilation du projet.

II-A-2 - Projet principal (C++)

Dans le dossier principal, celui qui contient le projet de l’exécutable (racine du dossier mainProject), on crée les fichiers suivants :

- main.cpp : il contient le point d’entrée de l’exécutable (fonction main) ;

- CMakeLists.txt : c’est le fichier qui indique à CMake que ce dossier est un projet CMake. Il renseigne également les règles qui devront être prises en compte lors de la génération de notre solution.

Comme indiqué au début, il n’y aura pas de code compliqué ici, on crée donc une simple fonction main dans notre fichier source :

1. #include <iostream>

3. int main(int argc, char* argv[])

4. {

5. std::cout << “Hello, World!” << std::endl;

6. return 0;

7. }

Ensuite il s’agit de renseigner le fichier CMakeLists.txt :

1. # Donner la version minimale de CMake à utiliser,

2. # Pour une compatibilité CUDA la version doit être supérieure ou égale

3. # à 3.9.

4. cmake_minimum_required(VERSION "3,9")

6. # À cause de la contrainte imposée par CUDA et renseignée plus haut,

7. # demander une compilation pour une plateforme cible 64 bits.

8. set(CMAKE_GENERATOR_PLATFORM "x64")

10. # Ensuite il faut déclarer le projet en renseignant les langages

11. # utilisés

12. project(mainProject CXX)

14. # On déclare ensuite que l’on veut produire un exécutable en renseignant

15. # les sources qui le composent.

16. add_executable(mainProject main.cpp)

Ici rien de compliqué, il faudra réaliser quelques modifications lorsque que le sous-projet de la bibliothèque CUDA sera mis en place.

II-A-3 - Projet de la bibliothèque CUDA

Il faut maintenant se déplacer dans le sous-dossier cudaLibProject. On y crée cette fois-ci trois fichiers :

- cudalib.cu : il s’agit du fichier source CUDA qui peut contenir aussi bien du code C++ pur que du code CUDA ;

- cudalib.h : il s’agit de la partie qui sera exposée à notre programme écrit en C++ uniquement. Une solution pour ne pas exposer de code CUDA dans ce fichier est étudiée plus loin ;

- CmakeLists.txt.

Pour le moment, les fichiers sources vont rester vides, ils seront remplis dans la prochaine partie.

De la même manière que le projet principal, il faut remplir le fichier CMakeLists.txt, mais cette fois pour compiler une bibliothèque :

1. # Donner la version minimum de CMake à utiliser,

2. # Pour une compatibilité CUDA la version doit être supérieure ou égale

3. # à 3.9.

4. cmake_minimum_required(VERSION "3,9")

6. # Ici, nul besoin de spécifier la plateforme source, la compilation

7. # ciblera automatiquement les plateformes 64 bits

9. # Ensuite, il faut déclarer le projet en renseignant les langages

10. # utilisés (donc CUDA et C++ dans ce cas)

11. project(cudaLibProject CUDA CXX)

13. # On déclare ensuite que l’on veut produire une bibliothèque statique

14. add_library(cudaLibProject STATIC cudalib.cu cudalib.h)

Il est également possible de produire une bibliothèque dynamique en remplaçant le mot clef STATIC par SHARED.

Une telle configuration permet de produire une bibliothèque statique qui pourra être liée à l’exécutable lors de la compilation du projet. Pour cela, il est nécessaire de modifier le CMakeLists.txt du projet principal pour qu’il utilise le sous-projet cudaLibProject.

Il faut donc indiquer que :

1. Le sous-projet de la bibliothèque CUDA existe et se trouve dans le sous-dossier cudaLibProject ;

2. L’édition de liens doit être réalisée.

Suite aux modifications, on obtient le fichier suivant :

1. # Donner la version minimale de CMake à utiliser,

2. # Pour une compatibilité CUDA la version doit être supérieure ou égale

3. # à 3.9.

4. cmake_minimum_required(VERSION "3,9")

6. # À cause de la contrainte imposée par CUDA et renseignée plus haut,

7. # demander une compilation pour une plateforme cible 64 bits.

8. set(CMAKE_GENERATOR_PLATFORM "x64")

10. # Ensuite il faut déclarer le projet en renseignant les langages

11. # utilisés

12. project(mainProject CXX)

14. # On ajoute le sous-projet au projet principal

15. add_subdirectory(cudaLibProject)

17. # On déclare ensuite que l’on veut produire un exécutable en renseignant

18. # les sources qui le composent.

19. add_executable(mainProject main.cpp)

21. # On précise que la bibliothèque produite par le sous-projet doit être

22. # liée à l’exécutable

23. target_link_library(mainProject cudaLibProject)

Ainsi, lors de la compilation, le sous-projet sera, si nécessaire, construit avant le projet principal puis lié à l’exécutable.

Il est maintenant temps de tester si nos projets sont bien en place.

II-B - Compilation du projet

Le but de CMake est de générer des fichiers permettant de construire un projet en fonction de la plateforme sur laquelle on se trouve. Avant de pouvoir compiler le code, il est donc nécessaire de passer par l’étape de génération de projet.

Pour cela, en se plaçant dans la racine du dossier du projet principal :

- on crée un dossier build. Ce dossier contiendra tous les éléments générés par l’exécution de CMake sur notre projet ;

- on se déplace dans le dossier build ;

- on exécute simplement la commande cmake ... On exécute donc cmake avec comme fichier d’entrée celui se trouvant à la racine de notre projet principal.

Si tout se passe correctement, une solution Visual Studio est alors générée (sous Windows). On peut donc passer à l’étape de compilation en utilisant les outils de ligne de commande de Visual C++.

Pour générer la solution, il faut utiliser l’outil MSBuild sur le fichier solution qui porte le nom du projet principal (pour le construire avec ses dépendances). Ici la commande est :

1. MSBuild mainProject.sln

Un exécutable sera produit et son exécution devrait retourner un « Hello, World! » dans la console. Si ce n’est pas le cas, il ne faut pas hésiter à tout passer en revue.

Il faut maintenant exposer les fonctions CUDA à notre programme C++ de sorte qu’il puisse les exploiter sans pour autant devoir être compilé avec le compilateur CUDA (nvcc).

III - Déclaration et interfaçage des fonctions CUDA

La solution étudiée ici semble largement utilisée lorsqu’il s’agit de se passer du compilateur nvcc pour la compilation d’un programme utilisant une bibliothèque contenant des fonctions CUDA.

Le principe est de laisser tout le code CUDA dans les fichiers sources qui seront par la suite compilés et de créer des fonctions C++ exposées dans les fichiers d’en-tête de la bibliothèque. Ces fonctions C++ réaliseront simplement un appel des fonctions CUDA.

En reprenant l’exemple du projet mis en place ici, on obtient alors le fichier cudalib.h suivant :

1. #ifndef CUDALIB_H

2. #define CUDALIB_H

4. void pTest();

6. #endif // CUDALIB_H

On expose une fonction C++ qui est donc accessible au programme principal sans l’utilisation de code CUDA ! Le fichier cudalib.cu lui contiendra le code suivant :

1. #include "processor.h"

3. __global__ void kernel_test()

4. {

5. // calcul multiprocesseur

6. }

8. void pTest()

9. {

10. kernel_test<<<1, 1>>>();

11. }

Ceci permet de faire appel à la fonction pTest() dans le programme et d’obtenir les avantages du calcul parallèle sur GPU.

IV - Remerciements

Il s’agit de mon premier article sur Developpez.com et je tiens à vivement remercier Laethy et Dourouc05 pour l’accompagnement et le soutien apportés tout au long de sa rédaction.