Blog entries by Damien Garaud [1]

Tester MPI avec CMake et un framework de test comme Boost

2014/06/20 by Damien Garaud

Objectif

Compiler et exécuter un fichier de test unitaire avec MPI.

Je suppose que :

  • une implémentation de MPI est installée (nous prendrons OpenMPI)
  • les bibliothèques Boost MPI et Boost Unit Test Framework sont présentes
  • vous connaissez quelques rudiments de CMake

CMake

On utilise le bien connu find_package pour Boost et MPI afin de récupérer tout ce qu'il nous faut pour les headers et les futurs links.

find_package (MPI REQUIRED)
find_package (Boost COMPONENTS mpi REQUIRED)

# Boost dirs for headers and libs.
include_directories (SYSTEM ${Boost_INCLUDE_DIR})
link_directories (${Boost_LIBRARY_DIRS})

Par la suite, on a essentiellement besoin des variables CMake :

  • Boost_MPI_LIBRARY pour le link avec Boost::MPI
  • MPI_CXX_LIBRARIES pour le link avec la bibliothèque OpenMPI
  • MPIEXEC qui nous donne la commande pour lancer un exécutable via MPI

On prend un fichier example_mpi.cpp (des exemples simples sont faciles à trouver sur la Toile). Pour le compiler, on fait :

set(CMAKE_CXX_COMPILER mpicxx)

# MPI example.
add_executable(example_mpi example_mpi.cpp)
target_link_libraries(example_mpi ${MPI_CXX_LIBRARIES})

voire juste

target_link_libraries(example_mpi ${Boost_MPI_LIBRARY})

si on décide d'utiliser la bibliothèque Boost MPI.

Note

mpicxx est une commande qui enrobe le compilateur (g++ par exemple). On dit à CMake d'utiliser mpicxx au lieu du compilo par défaut.

Note

Boost::MPI n'est pas une implémentation de MPI. C'est une bibliothèque plus haut niveau qui s'abstrait de l'implémentation de MPI. Il faut nécessairement en installer une (OpenMPI, LAM/MPI, MPICH2, ...).

L'exemple peut très simplement ressembler à :

#include <boost/mpi/environment.hpp>
#include <boost/mpi/communicator.hpp>
#include <iostream>

namespace mpi = boost::mpi;

int main()
{
  mpi::environment env;
  mpi::communicator world;
  std::cout << "I am process " << world.rank() << " of " << world.size()
            << "." << std::endl;
  return 0;
}

Exécuter

Une fois la compilation effectuée, faire simplement :

> mpiexec -np 4 ./example_mpi

pour lancer l'exécutable sur 4 cœurs.

Problème : mais pourquoi tu testes ?

On veut pouvoir faire des exécutables qui soient de vrais tests unitaires et non pas un exemple avec juste une fonction main. De plus, comme j'utilise CMake, je veux pouvoir automatiser le lancement de tous mes exécutables via CTest.

Problème : il faut bien initialiser et bien dire à MPI que j'en ai fini avec toutes mes MPI-series.

En "vrai" MPI, on a :

int main(int argc, char* argv[])
{
  MPI_Init(&argc, &argv);

  int rank;
  MPI_Comm_rank(MPI_COMM_WORLD, &rank);
  // Code here..
  // ... and here
  MPI_Finalize();

  return 0;
}

Note

C'est ce que fait le constructeur/destructeur de boost::mpi::environment.

En d'autres termes, je veux me faire ma propre fonction main pour l'initialisation de tous mes cas tests Boost (ou avec n'importe quel autre framework de test unitaire C/C++).

La documentation de Boost Unit Test, que je trouve parfois très peu claire avec un manque cruel d'exemple, m'a fait galérer quelques heures avant de trouver quelque chose de simple qui fonctionne.

Conseil : aller regarder les exemples des sources en faisant quelques grep est parfois plus efficace que de trouver la bonne info dans la doc en ligne. On peut aussi en lire sur https://github.com/boostorg/test/tree/master/example

Deux solutions :

  1. la première que j'ai trouvée dans les tests de Boost::MPI lui-même. Ils utilisent le minimal testing facility. Mais seule la macro BOOST_CHECK est utilisable. Et oubliez les BOOST_CHECK_EQUAL ainsi que l'enregistrement automatique de vos tests dans la suite de tests.
  2. la deuxième redéfinit la fonction main et appelle boost::unit_test::unit_test_main sans définir ni la macro BOOST_TEST_MODULE ni BOOST_TEST_MAIN qui impliquent la génération automatique de la fonction main par le framework de test (que l'on compile en statique ou dynamique). Pour plus de détails, lire http://www.boost.org/doc/libs/release/libs/test/doc/html/utf/user-guide/test-runners.html

J'utiliserai la deuxième solution.

Note

Ne pensez même pas faire une fixture Boost pour y mettre votre boost::mpi::environment puisque cette dernière sera construite/détruite pour chaque cas test (équivalent du setUp/tearDown). Et il est fort probable que vous ayez ce genre d'erreur :

*** The MPI_Errhandler_set() function was called after MPI_FINALIZE was invoked.
*** This is disallowed by the MPI standard.
*** Your MPI job will now abort.
[hostname:11843] Abort after MPI_FINALIZE completed successfully; not able to guarantee that all other processes were killed!

Un exemple qui marche

On souhaite ici tester que le nombre de procs passés en argument de mpiexec est au moins 2.

Le BOOST_TEST_DYN_LINK dit juste que je vais me lier dynamiquement à Boost::Test.

#define BOOST_TEST_DYN_LINK
#include <boost/test/unit_test.hpp>

#include <boost/mpi/environment.hpp>
#include <boost/mpi/communicator.hpp>

namespace mpi = boost::mpi;

BOOST_AUTO_TEST_CASE(test_check_world_size)
{
    mpi::communicator world;
    BOOST_CHECK(world.size() > 1);
}


// (empty) Initialization function. Can't use testing tools here.
bool init_function()
{
    return true;
}

int main(int argc, char* argv[])
{
    mpi::environment env(argc, argv);
    return ::boost::unit_test::unit_test_main( &init_function, argc, argv );
}

On lance tout ça avec un joli mpiexec -np 2 ./test_mpi et on est (presque) content.

Et un peu de CTest pour finir

Une dernière chose : on aimerait dire à CMake via CTest de lancer cet exécutable, mais avec mpiexec et les arguments qui vont bien.

Un cmake --help-command add_test nous indique qu'il est possible de lancer n'importe quel exécutable avec un nombre variable d'arguments. On va donc lui passer : /usr/bin/mpiexec -np NB_PROC ./test_mpi.

Un œil au module FindMPI.cmake nous dit aussi qu'on peut utiliser d'autres variables CMake MPI pour ce genre de chose.

Reprenons donc notre fichier CMake et ajoutons :

add_executable(test_mpi test_mpi.cpp)
target_link_libraries(test_mpi ${Boost_MPI_LIBRARY})
# Number of procs for MPI.
set (PROCS 2)
# Command to launch by CTest. Need the MPI executable and the number of procs.
add_test (test_mpi ${MPIEXEC} ${MPIEXEC_NUMPROC_FLAG} ${PROCS}
   ${MPIEXEC_PREFLAGS}
   test_mpi
   ${MPIEXEC_POSTFLAGS})

où mon exécutable et mon test porte le même nom : test_mpi. On lance le tout avec la commande ctest dans notre dossier de build et hop, le tour est joué !


Lecture de données tabulées avec Numpy -- Retour d'expérience sur dtype

2013/12/17 by Damien Garaud

Ce billet est un petit retour d'expérience sur l'utilisation de Numpy pour lire et extraire des données tabulées depuis des fichiers texte.

Chaque section, hormis les objectifs ou la conclusion, correspond soit à une difficulté rencontrée, une remarque technique, des explications et références vers la documentation officielle sur un point précis qui m'a fait patauger quelques temps. Il y a de forte chance, pour certains d'entre vous, que les points décrits ici vous paraissent évidents, que vous vous disiez "mais qui ne sait pas ça ?!". J'étais moi-même le premier étonné, depuis que je connais Numpy, de ne pas savoir ce genre de choses. Je l'étais moins quand autour de moi, mes camarades ne semblaient pas non plus connaître les petites histoires numpysiennes que je vais vous conter.

http://www.logilab.org/file/203839/raw/numpylogo.png

Objectifs

Le Pourquoi et le Où on va au fait ?

J'avais sous la main des fichiers aux données tabulées, type CSV, où les types de données par colonne étaient clairement identifiés. Je ne souhaitais pas passer du temps avec le module csv de la bibliothèque standard à convertir chaque élément en type de base, str, flottants ou dates. Numpy étant déjà une dépendance du projet, et connaissant la fonction np.genfromtxt, j'ai évidemment souhaité l'utiliser.

Il était nécessaire de ne lire que certaines colonnes. Je souhaitais associer un nom à chaque colonne. L'objectif était ensuite d'itérer sur ces données ligne par ligne et les traiter dans des générateurs Python. Je n'utilise pas ici Numpy pour faire des opérations mathématiques sur ces tableaux à deux dimensions avec des types hétérogènes. Et je ne pense d'ailleurs pas qu'il soit pertinent d'utiliser ce type de tableau pour faire ces opérations.

dtypes différents, str et extraction de chaînes vides

On utilise ici l'argument dtype des fonctions telles que np.genfromtxt pour lire des fichiers tabulés dont les colonnes sont de types différents.

Attention au dtype à passer à np.genfromtxt ou np.recfromtxt quand on parse des données tabulée (file ou stream). Pour parser une colonne de chaînes de caratères, lui passer [('colname', str)] renvoie des chaînes vides si les autres dtypes sont de types différents.

Il faut préciser la taille :

dtype=[('colname', str, 10)]
# or
dtype=[('colname', 'S10')]

Ou alors prendre un "vrai" objet str Python :

dtype=[('colname', object)]

aussi équivalent à:

dtype=[('colname', 'object')]

Et oui, je suis littéralement tombé sur l'évidence, les "types Numpy", c'est du type C. Et les chaînes, c'est du char * et il y a donc besoin de la taille. La solitude s'est fait moindre quand j'ai su que je n'étais pas le seul à être tombé sur des données tronquées voire vides.

dtype et tableau à zéro dimension

Attention au tableau Numpy 0D quand le contenu tabulé à parser n'a qu'une seule ligne (cas d'un np.[rec]array avec plusieurs dtypes). Impossible d'itérer sur les éléments puisque dimension nulle.

Supposons que vous ayez un fichier tabulé d'une seule ligne :

Name,Play,Age
Coltrane,Saxo,28

J'utilise np.genfromtxt en précisant le type des colonnes que je souhaite récupérer (je ne prends pas en compte ici la première ligne).

data = np.genfromtxt(filename, delimiter=',',
                     dtype=[('name', 'S12'), ('play', object), ('age', int)],
                     skip_header=1)

Voici la représentation de mon array :

array(('Coltrane', 'Saxo', 28),
    dtype=[('name', 'S12'), ('play', 'O'), ('age', '<i8')])

Si dans votre code, vous avez eu la bonne idée de parcourir vos données avec :

for name, instrument, age in data:
    # ...

vous pourrez obenir un malheureux TypeError: 'numpy.int64' object is not iterable par exemple. Vous n'avez pas eu de chance, votre tableau Numpy est à zéro dimension et une shape nulle (i.e. un tuple vide). Effectivement, itérer sur un objet de dimension nulle n'est pas chose aisée. Ce que je veux, c'est un tableau à une dimension avec un seul élément (ici un tuple avec mes trois champs) sur lequel il est possible d'itérer.

Pour cela, faire:

>>> print data
array(('Coltrane', 'Saxo', 28),
      dtype=[('name', 'S12'), ('play', 'O'), ('age', '<i8')])array(('babar', 42.), dytpe=[('f0', 'S5'), ('f1', '<f8')])
>>> print data.shape, data.ndim
(), 0
>>> data = data[np.newaxis]
>>> print data.shape, data.ndim
(1,), 1

dtype et str : chararray ou ndarray de strings ?

Pour les chararray, lire help(np.chararray) ou http://docs.scipy.org/doc/numpy/reference/generated/numpy.chararray.html. En particulier:

The chararray class exists for backwards compatibility with Numarray, it is not recommended for new development. Starting from numpy 1.4, if one needs arrays of strings, it is recommended to use arrays of dtype object_, string_ or unicode_, and use the free functions in the numpy.char module for fast vectorized string operations.

On fera donc la distinction entre:

# ndarray of str
na = np.array(['babar', 'celeste'], dtype=np.str_)
# chararray
ca = np.chararray(2)
ca[0], ca[1] = 'babar', 'celeste'

Le type de tableau est ici différent : np.ndarray pour le premier et np.chararray pour le second. Malheureusement pour np.recfromtxt et en particulier pour np.recarray, si on transpose le label de la colonne en tant qu'attribut, np.recarray il est transformé en chararray avec le bon type Numpy --- |S7 dans notre cas --- au lieu de conserver un np.ndarray de type |S7.

Exemple :

from StringIO import StringIO
rawtxt = 'babar,36\nceleste,12'
a = np.recfromtxt(StringIO(rawtxt), delimiter=',', dtype=[('name', 'S7'), ('age', int)])
print(type(a.name))

Le print rend bien un objet de type chararray. Alors que :

a = np.genfromtxt(StringIO(rawtxt), delimiter=',', dtype=[('name', 'S7'), ('age', int)])
print(type(a['name']))

affiche ndarray. J'aimerais que tout puisse être du même type, peu importe la fonction utilisée. Au vue de la documentation et de l'aspect déprécié du type charray, on souhaiterait avoir que du ndarray de type np.str. J'ai par ailleurs ouvert le ticket Github 3993 qui n'a malheureusement que peu de succès :-(

Tableau de chaînes : quel dtype ?

Si certains se demandent quoi mettre pour représenter le type "une chaîne de caractères" dans un tableau numpy, ils ont le choix :

np.array(['coltrane', 'hancock'], dtype=np.str)
np.array(['coltrane', 'hancock'], dtype=np.str_)
np.array(['coltrane', 'hancock'], dtype=np.string_)
np.array(['coltrane', 'hancock'], dtype='S')
np.array(['coltrane', 'hancock'], dtype='S10')
np.array(['coltrane', 'hancock'], dtype='|S10')

Les questions peuvent être multiples : est-ce la même chose ? pourquoi tant de choses différentes ? Pourquoi tant de haine quand on lit la doc Numpy et que l'info ne saute pas aux yeux ? À savoir que le tableau construit sera identique dans chacun des cas. Il existe peut-être même d'autres moyens de construire un tableau de type identique en lui passant encore un n-ième argument dtype différent.

  • np.str représente le type str de Python. Il est converti automatiquement en type chaines de caractère Numpy dont la longueur correspond à la longueur maximale rencontrée.
  • np.str_ et np.string_ sont équivalents et représentent le type "chaîne de caractères" pour Numpy (longueur = longueur max.).
  • Les trois autres utilisent la représentation sous forme de chaîne de caractères du type np.string_.
    • S ne précise pas la taille de la chaîne (Numpy prend donc la chaîne la plus longue)
    • S10 précise la taille de la chaîne (données tronquées si la taille est plus petite que la chaîne la plus longue)
    • |S10 est strictement identique au précédent. Il faut savoir qu'il existe cette notation <f8 ou >f8 pour représenter un flottant. Les chevrons signifient little endian ou big endian respectivement. Le | sert à dire "pas pertinent". Source: la section typestr sur la page http://docs.scipy.org/doc/numpy/reference/arrays.interface.html

À noter que j'ai particulièrement apprécié l'utilisation d'un symbole pour spécifier une information non pertinente --- depuis le temps que je me demandais ce que voulait bien pouvoir dire ce pipe devant le 'S'.

Conclusion (et pourquoi pas pandas ?)

http://www.logilab.org/file/203841/raw/pandas_logo.png

Pandas, bibliothèque Python d'analyse de données basée sur Numpy, propose, via sa fonction read_csv, le même genre de fonctionnalités. Il a l'avantage de convertir les types par colonne sans lui donner d'information de type, qu'on lise toutes les colonnes ou seulement quelques unes. Pour les colonnes de type "chaîne de caractères", il prend un dtype=object et n'essaie pas de deviner la longueur maximale pour le type np.str_. Vous ne rencontrerez donc pas "le coup des chaînes vides/tronquées" comme avec dtype='S'.

Je ne m'étalerai pas sur tout le bien que je pense de cette bibliothèque. Je vous invite par ailleurs à lire/ parcourir un billet de novembre qui expose un certain nombre de fonctionnalités croustillantes et accompagné d'un IPython Notebook.

Et pourquoi pas Pandas ? Il ne me semble pas pertinent de dépendre d'une nouvelle bibliothèque, aussi bien soit-elle, pour une fonction, aussi utile soit-elle. Pandas est un projet intéressant, mais jeune, qui ne se distribue pas aussi bien que Numpy pour l'instant. De plus, le projet sur lequel je travaillais utilisait déjà Numpy. Je n'avais besoin de rien d'autre pour réaliser mon travail, et dépendre de Pandas ne me semblait pas très pertinent. Je me suis donc contenté des fonctions np.{gen,rec}fromtxt qui font très bien le boulot, avec un dtype comme il faut, tout en retenant les boulettes que j'ai faites.


Rencontre LLVM à l'IRILL

2012/09/27 by Damien Garaud
IRILL-logoLLVM-logo

L'IRILL , Initiative de Recherche et Innovation sur le Logiciel Libre, organisait une rencontre LLVM mardi 25 septembre à Paris dans les locaux de l'INRIA Place d'Italie dans le 13e arrondissement. Les organisateurs, Arnaud de Grandmaison, Duncan Sands, Sylvestre Ledru et Tobias Grosser ont chaleureusement accueilli environ 8 personnes dont 3 de Logilab.

L'annonce de l'IRILL indiquait une rencontre informelle avec une potentielle Bug Squashing Party sur Clang. Nous n'avons pas écrasé d'insectes mais avons plutôt discuté: discussions techniques, petits trolls, échanges de connaissances, d'outils et d'expériences accompagnés de bières, sodas, gâteaux et pizzas (et une salade solitaire et orpheline qui n'a finie dans le ventre de personne contrairement aux pizzas mexicaines ou quatre fromages).

LLVM (Low Level Virtual Machine) est un projet développé depuis une dizaine d'années qui propose une collection d'outils et de modules facilement réutilisables pour construire des logiciels orientés langages : interpréteurs, compilateurs, optimiseurs, convertisseurs source-to-source...

Depuis l'étage d'entrée du code dans le compilateur (ou frontend), en passant par l'optimiseur indépendant de la plate-forme, jusqu'à la génération de code machine (backend) et ce, pour plusieurs architectures (X86, PowerPC, ARM, etc.), le choix de son design en fait un outil tout à fait intéressant. Parmi ces frontends, il y a le fameux Clang (C/C++, Objective-C), GHC (Haskell). Et le projet dragonegg permet de l'utiliser comme plug-in à GCC et donc de bénéficier des différents frontends GCC (Ada, Fortran, etc.).

De nombreux outils se sont construits autour du framework LLVM : LLDB un débugger, vmkit une implémentation de la machine virtuelle Java et .NET ou encore polly, un projet de recherche dont Tobias est l'un des deux co-fondateurs, qui a pour objectif d'optimiser un programme indépendamment du langage via des polyhedral optimizations.

Les principaux contributeurs à ce jour sont Apple, Google, des contributeurs "individuels" dont Duncan Sands, suivis par des laboratoires de recherche et autres. Voir l'article de Sylvestre sur "Who is in control of LLVM/Clang ?".

La clef de voûte de LLVM est sa représentation intermédiaire (IR pour Intermediate Representation). C'est une structure de données qui représente sous forme SSA (Single Static Assignment) les flux de données et de contrôle. Cette forme est plus "haut-niveau" et plus lisible que du code assembleur, elle comporte certaines informations de type par exemple. Elle constitue le pivot de l'infrastructure LLVM : c'est ce que produisent les frontends comme Clang, qui est ensuite passée aux optimiseurs de LLVM et est ensuite consommée par les backends dont le rôle est de les transformer en code machine natif.

En voici un exemple en C:

int add(int a)
{
    return a + 42;
}

La représentation intermédiaire est donnée via Clang :

$> clang -S -emit-llvm add_function.c -o add_function.ll

L'extension *.ll désigne des "fichiers IR" et le résultat donne:

define i32 @add(i32 %a) nounwind uwtable {
  %1 = alloca i32, align 4
  store i32 %a, i32* %1, align 4
  %2 = load i32* %1, align 4
  %3 = add nsw i32 %2, 42
  ret i32 %3
}

Cette représentation peut ensuite être bitcodée, assemblée ou compilée. Voir les différentes commandes LLVM pour assembler, désassembler, optimiser, linker, exécuter, etc.

Une autre utilisation intéressante de cette infrastructure est l'utilisation de la bibliothèque Clang pour parser du code C/C++ afin de parcourir l'Abstract Syntax Tree (AST). Ceci offre notamment de belles possibilités d'introspection. Google a d'ailleurs rédigé un tutoriel sur les plugins Clang et ses possibilités via l'API de Clang, notamment la classe ASTConsumer. Il est possible de parcourir l'ensemble des constructeurs, de connaître la propriété d'une classe (abstraite ou non), parcourir les membres d'une classe, etc. Avant de partir, nous parlions de la possiblité de propager un changement d'API à travers toutes les bibliothèques qui en dépendent, soit la notion de patch sémantique.

Nous avons aussi profité pour parler un peu du langage Julia ou de Numba.

Pythonistes convaincus, nous connaissions déjà un peu Numba, projet de Travis Oliphant, contributeur NumPy et papa de SciPy. Ce projet utilise l'infrastructure LLVM pour compiler du byte-code Python en code machine (Just In Time compilation), en particulier pour NumPy et SciPy. Les exemples montrent comment passer d'une fonction Python qui traite un tableau NumPy à une fonction compilée Just In Time. Extrait issu d'un des exemples fournis par le projet Numba :

from numba import d
from numba.decorators import jit as jit

def sum2d(arr):
    M, N = arr.shape
    result = 0.0
    for i in range(M):
        for j in range(N):
            result += arr[i,j]
    return result

csum2d = jit(ret_type=d, arg_types=[d[:,:]])(sum2d)

Oui, la fonction sum2d n'est pas optimale et très "naïve". Néanmoins, la fonction compilée va près de 250 fois plus vite ! Numba utilise les bindings LLVM pour Python via llvm-py afin de passer du code Python, à l'Intermediate Representation pour ensuite utiliser les fonctionnalités JIT d'LLVM.

Entre programmeurs de langages à typage statique, nous avons aussi parlé de C et d'Ocaml (dont il existe des bindings pour LLVM) et mentionné de beaux projets tels que PIPS et Coccinelle.

Pour finir, nous savons maintenant prononcer Clang. On dit "klang" et non "cilangue". Nous avons appris que gdb avait son propre parser et n'utilisait pas le parser de GCC. Rappelons-le, l'un des grands avantages de LLVM & Clang face à GCC est sa modularité et la possibilité d'utiliser une des pierres qui forme l'édifice pour construire sa propre maison.

Nous finissons ce billet par une citation. David Beazley disait lors de sa présentation à Eursoscipy 2012 :

The life is too short to write C++.

Certes. Mais qu'on le veuille ou qu'on le doive, autant se servir d'outils biens pensés pour nous faciliter la vie.

Encore merci aux organisateurs pour cette rencontre et à la prochaine.

Quelques références