Etudiant ingénieur en Informatique spécialisé en imagerie

Passioné par la programmation graphique et la création de jeux vidéo, j'aime créer de nouvelles choses en partant de zéro ou en utilisant des moteurs de jeu de référence dans l'industrie comme Unity3D.

Information
Belfort, France
Français
(+33)669 606 859
thomas.gredin@utbm.fr
Réseaux sociaux

Mise en place d'un environnement de développement CUDA/C++ sur Windows


nvdia_cuda

Dans le cadre d'un projet je me suis confronté à la mise en place d'un environnement de développement pour CUDA/C++. Il s'agit de la poursuite/refonte d'un projet qui utilise actuellement le gestionnaire de projet QMake. Pour faire court le rôle de l'application est de faire du recalage entre deux images consécutive d'une vidéo (flux optique) et donc ainsi réaliser du tracking d'objet. Les calculs sont réalisés sur le GPU (d'où l'utilisation de CUDA) et un rendu du résultat est exprimé dans une scène 3D, sous forme d'un maillage, réalisée avec OpenGL.

Les contraintes pour le nouvel environnement à mettre en place sont les suivantes :

  • Ne plus utiliser QMake
  • Ne plus utiliser Qt
  • Ne pas utiliser l'IDE Visual Studio

En effet dans ce que je présente ici je n'utilise pas Visual Studio, cependant une des premières contraintes du Toolkit CUDA sur Windows est qu'il n'utilise que le compilateur hôte cl.exe qui est celui de Microsoft. La solution que j'apporte ici n'est peut-être pas la meilleur mais l'environnement fonctionne et son utilisation n'est pas déplaisante (Si on est pas allergique à la console...).

Mon idée de base est donc de passer la gestion de projet sous CMake qui supporte nativement CUDA depuis quelques versions déjà. Ainsi on pourra utiliser l'éditeur de code que l'on souhaite sans soucis.

Installation des outils nécessaires

Avant de commencer à configurer l'environnement et créer un premier projet il faut s'assurer que tous les outils sont correctement installés. Dans l'ordre :

  1. Visual Studio Build Tools : contient le compilateur et les outils de compilation de projet qui vont servir par la suite. Il semblerait d'ailleurs que cette outil n'existe plus en version indépendante, j'ai donc installé Visual Studio qui vient avec tous les éléments y étaient.
  2. Compilateur CUDA : pour compiler des programmes CUDA...
  3. Cmake : donne accès à l'utilitaire en ligne commande. Offre également une interface graphique pour les plus réfractaires !

Une fois tous les éléments installés je commence la configuration de mon environnent ! La configuration que je présente me convient parfaitement mais elle peut déplaire à d'autres. Ce n'est pas une solution unique et chacun met en place l'environnement qui lui plaît le plus.

ATTENTION : Pour pouvoir exécuter des programmes CUDA il est nécessaire que votre machine soit équipée d'un GPU Nvidia compatible avec la technologie. Vérifier si le votre est compatible ici. Si votre GPU est plutôt récent il ne devrait pas y avoir de soucis et si il n'est pas compatible alors l'installateur devrait l'indiquer !

Configuration de l'environnement

J'utilise Cmder comme émulateur de terminal sous Windows. C'est un outil bien plus convivial que le terminal Windows de base car il donne accès à certaines commandes que l'on retrouve dans le Bach Linux. Il offre également des fonctionnalités comme la définition d'alias et l'exécution d'un script lors du démarrage.

Le site officiel le présente bien mieux que moi !

Visual Studio Build Tools

Tout d'abord il faut commencer par pouvoir accéder aux outils du Visual Studio Build Tools depuis ce terminal. Dans le dossier d'installation de Visual Studio on peut retrouver une série de scripts .bat qui permettent de mettre en place les chemins des différents exécutables suivant la configuration choisie. Ses scripts se trouve dans le répertoire :

C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build

Le chemin peut être différent chez vous. Le script qui va nous intéresser dans ce dossier est vcvarsall.bat. J'ai décidé de créer un alias dans le fichier de configuration de Cmder pour appeler ce script quand je le souhaite. Ce fichier se trouve dans le dossier config de l'application et se nomme _useraliases. Il suffit de rajouter une ligne à la fin :

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

Ici je créer un alias qui appelle le script en précisant qu'on souhaite les outils pour une architecture cible en 64bits.

CUDA (ou plutôt sont compilateur NVCC) ne supporte plus la compilation 32bits depuis la version 2013 de Visual Studio

Une autre mise en place possible est d’appeler le script directement au démarrage du terminal en mettant cette commande dans le fichier _userprofile de la manière suivante :

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

Normalement aucune action n'est requise car l'installateur met automatiquement les bons chemins dans la variable d'environnement PATH.

Cmake

De même.

Configuration d'un projet avec CMake

Il est maintenant temps de rentrer dans le dur... J'ai vraiment eu du mal à comprendre certains comportements que je pouvais obtenir avec CMake et NVCC mais j'ai réussi à me dépatouiller et je vais essayer de faire un rapport détaillé de ma démarche. A savoir que c'était un de mes premiers contact avec CMake et que je ne suis donc pas un professionnel !

Préparation d'un projet de test

Tout d'abord je commence par créer un simple fichier CUDA qui servira à tester le fichier CMakeLists. Ce fichier permet de définir comment va être générée la solution en spécifiant différentes options et les fichiers source du projet. Je créer une arborescence simple qui servira de base :

Arborescence de base

le fichier source main.cu est un HelloWorld qui réalise juste un appel d'une fonction qui s'exécute sur le GPU :

/* main.cu */
#include <iostream>

__global__
void swap(int& px, int& py)
{
    int tmp = px;
    px = py;
    py = tmp;
}

int main()
{
    int* x;
    int* y;

    cudaMallocManaged(&x, sizeof(int));
    cudaMallocManaged(&y, sizeof(int));

    *x = 5;
    *y = 8;

    std::cout << "Hello" << std::endl;
    std::cout << "GPU process... : " << *x << "  " << *y << std::endl;

    swap<<<1, 1>>>(*x, *y);
    cudaDeviceSynchronize();

    std::cout << "Process ended -> x = " << *x << ", y = " << *y << std::endl;

    cudaFree(x);
    cudaFree(y);

    return 0;
}

Un code très simple qui ne tir absolument pas partie des avantages du calcul sur GPU... Cela dit il est censé compiler et afficher les valeur de x et y avant de se terminer (j'ai quand même testé et ça marche). Jusqu'ici rien de compliqué, on peut compiler le programme en utilisant directement nvcc dans le terminal seulement je ne souhaite pas devoir taper la commande à chaque fois... Il est important que l'environnement de développement soit mit en place avant de lancer la commande (voir la partie sur la mise en place d'un alias pour appeller le script vcvarall.bat), si ce n'est pas le cas alors nvcc fera savoir qu'il ne trouve pas le compilateur hôte cl.exe.

Création du CMakeLists

La prise de tête à commencé ici ! Si j'ai bien tout compris, avant d'être entièrement supporté par CMake (à partir de la version 3.10), les projet CUDA devait utiliser un module (FindCUDA). Comme à chaque fois dans de tel cas on se retrouve perdu lorsque l'on cherche des solutions sur internet car l'ancienne manière de faire est toujours présente dans les réponses... Heureusement on peut toujours compter sur celui qui viendra clamer haut et fort que maintenant c'est fini, ce n'est plus comme ça que l'on fait.

J'utilisais donc l'ancienne méthode (que je ne parvennait pas à faire fonctionner) jusqu'à tomber sur cette page de la documentation officielle. CUDA peut donc être utilisé, et dois l'être, sans le module FindCUDA depuis la version 3.10 de CMake.

Je vais donc commencer la construction de mon fichier CMakeLists qui permettra de générer une solution que l'on pourra utiliser pour compiler le programme avec les outils Microsoft. Sur Linux et MacOS il s'agira de la génération d'un Makefile.

Une chose importante à savoir est que la configuration utilisé ne permet pas de compiler des programme pour des architectures 32 bits. Si on ne le précise pas alors la génération de la solution échouera. C'est une contrainte de nvcc lorsque que le compilateur hôte est d'une version supérieur à Visual Studio 2013. Pour préciser l'architecture cible on ajoute donc cette ligne au début de notre fichier :

# Set 64bit plateform
set(CMAKE_GENERATOR_PLATFORM "x64")

Ainsi le compilateur passé en paramètre à nvcc sera bien la version 64 bits de cl.exe. Avant d'ajouter cette simple ligne je ne faisait que rajouter l'argument -ccbin en spécifiant le chemin vers la version 64 bits de cl.exe. J'ai appris à mes dépens que faire ainsi ne fonctionne pas car quoi que l'on fasse des arguments par défaut sont quand même passé et donc cette configuration créait un doublon de cet argument ce qui avait pour effet de faire échouer la génération du projet.

L'étape suivante est de spécifier les chemins d'accès aux différents compilateurs :

# Set CUDA compiler path
set(
    CMAKE_CUDA_COMPILER 
    "C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v10.1/bin/nvcc.exe"
)
# Set cl.exe path for CUDA
set(
    CMAKE_CUDA_HOST_COMPILER
    "C:/Program Files (x86)/Microsoft Visual Studio/2017/Community/VC/Tools/MSVC/14.16.27023/bin/Hostx64/x64"
)

La dernière étape est de créer le projet et de définir l'exécutable qui sera produit lors de la compilation :

# Setup the project with used languages
project("CUDA_TESTS" CUDA CXX)
find_package(CUDA)

# Setup executable
add_executable("${PROJECT_NAME}" main.cu)

Ici on reste dans du CMake classique à la seule différence que je spécifie le langage CUDA en plus de C++. Le fichier CMakeLists que j'obtient au final est le suivant :

cmake_minimum_required(VERSION "3.14.0")

# Set 64bit plateform
set(CMAKE_GENERATOR_PLATFORM "x64")

# Set CUDA Flags for all configurations (empty for the time being)
set(CMAKE_CUDA_FLAGS "")
set(CMAKE_CUDA_FLAGS_DEBUG "")
set(CMAKE_CUDA_FLAGS_RELEASE "")
set(CMAKE_CUDA_FLAGS_RELWITHDEBINFO "")
set(CMAKE_CUDA_FLAGS_MINSIZEREL "")

# Set C++ compiler (VS2017 x64)
set(CMAKE_CXX_COMPILER "C:/Program Files (x86)/Microsoft Visual Studio/2017/Community/VC/Tools/MSVC/14.16.27023/bin/Hostx64/x64")

# Set CUDA compiler path
set(
    CMAKE_CUDA_COMPILER 
    "C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v10.1/bin/nvcc.exe"
)
# Set cl.exe path for CUDA
set(
    CMAKE_CUDA_HOST_COMPILER
    "C:/Program Files (x86)/Microsoft Visual Studio/2017/Community/VC/Tools/MSVC/14.16.27023/bin/Hostx64/x64"
)

# Show CMake version
message(STATUS "Cuda/C++ project creation begin... [CMAKE ${CMAKE_VERSION}]")

# Setup the project with used languages
project("CUDA_TESTS" CUDA CXX)
find_package(CUDA)

# Setup executable
add_executable("${PROJECT_NAME}" main.cu)

install(TARGETS "${PROJECT_NAME}" DESTINATION bin)
install(FILES "main.cu" DESTINATION src)

Il contient surement des options en trop mais il fait le café... La suite est de voir comment il va évoluer au cours du projet et si il saura répondre à mes besoins en terme linkage de bibliothèques C++ sans pour autant poser de soucis pour la partie CUDA. Ceci reste mon premier contact (et il est très naïf) avec CUDA et j'attends d'en faire plus pour m'en faire un avis. Peut être que je reviendrais pousser une gueulante dans quelques semaines qui sait ?

La génération du projet donne lieu à la création d'une solution Visual Studio. Pour la compiler deux solutions possibles. Soit on utilise directement l'IDE, cependant les contraintes du projet ne le permettent pas, soit on utilise l'outil MsBuild en ligne de commande. La deuxième solution est celle que j'utilise.

Si vous avez des remarques n'hésitez pas à me contacter pour me les faire parvenir !

Thomas Gredin.

Publié le 26/03/2019
Tags du poste

CUDA/C++