gKit2 light
|
Les méthodes Monte Carlo utilisent beaucoup de nombres aléatoires, on a déjà vu dans Monte Carlo, éclairage direct : structurer le code comment utiliser les générateurs standards et une manière de les utiliser dans du code parallèlisé.
La solution proposée dans Monte Carlo, éclairage direct : structurer le code initialise un générateur différent par ligne de l'image à calculer et par thread de calcul :
rappel : on ne veut surtout pas utiliser un seul générateur commun à tous les threads. ce serait une variable partagée, et il faudrait donc synchroniser son accès (avec une section critique par exemple). ca se fonctionnerait même pas très bien, en plus d'être très lent. du coup, la meilleure solution est d'initialiser un générateur par thread, au minimum. la solution la plus simple pour la boucle de calcul d'image est d'initialiser un générateur par ligne puisque openMP va créer un thread par ligne de l'image.
ce qui est simple, plutôt facile à utiliser et très efficace, mais qui a un gros défaut : l'image calculée sera différente à chaque exécution du programme. c'est même pire, l'ordre d'exécution des threads va aussi modifier le résultat. si on souhaite produire la même image avec un thread ou plusieurs, il va falloir cogiter un peu plus...
comment reproduire la même image que celle calculée par un seul thread et un seul générateur ?
il faut se rappeler qu'un générateur de nombres aléatoires génère une séquence de nombres pseudo aléatoires, ie toujours les mêmes, en fonction de son initialisation.
si on suppose que chaque pixel utilise \( n \) nombres aléatoires : le 1er pixel utilise les premiers nombres aléatoires d'indices \( [0 \, n) \), le pixel suivant les nombres aléatoires d'indices \( [n \, 2n) \), etc. pour produire la même image, quelque soit l'ordonnancement et le nombre de threads, il suffit d'utiliser les nombres aléatoires avec les mêmes indices pour chaque pixel de l'image...
il faut donc un moyen d'initialiser un générateur pour produire des nombres aléatoires à partir du ieme, ie pour un indice quelconque... mais les générateurs classiques de la stl ne savent pas faire ça, ie produire le ieme nombre aléatoire sans produire tous les précédents... il va falloir regarder un peu plus en détail comment fonctionnent ces générateurs.
de manière générale, on considère qu'un générateur est décrit par une fonction \( f(x) \) qui génère un nombre aléatoire à partir du précédent :
\[ x_{i}= f(x_{i-1}) = ax_{i-1} + c \mod m\\ \mbox{avec } x_0 = \mbox{seed} \]
et std::default_random_engine utilise les constantes (cf minstd_rand0) : \( a= 16807 \), \( m= 2^{31} -1\) et \( c= 0 \). on appelle ce type de générateur un LCG pour générateur linéaire (à cause de \(ax+c\)) congruentiel (à cause du modulo).
cette définition classique ne permet que de calculer un nouveau terme à partir du précédent, mais il est quand même possible de calculer le ieme terme en fonction du premier, cf "Random number generation with arbitrary strides", F.B. Brown 1994 !
\[ x_{i}= f(x_0)= x_0a^i + c \frac{a^i -1}{a-1} \mod m \]
par contre, il faut calculer \(a^i\) pour générer le ieme nombre aléatoire et ça ne va pas être très rapide, même si on peut utiliser un algorithme diviser-pour-régner plus rapide que calculer i fois le produit...
on peut écrire un générateur LCG avec cette nouvelle fonction, index(i)
qui permet de générer des nombres aléatoires à partir du ieme terme. la taille des entiers ainsi que les constantes \( a \) et \( c \) sont des paramètres template, ce qui permet d'écrire les versions 32 et 64 bits de manière compacte :
on peut maintenant produire les N premiers nombres aléatoires dans n'importe quel ordre !!
il est tout à fait raisonnable d'utiliser ce générateur pour calculer une image, en utilisant index()
une seule fois par pixel :
dernière chose à vérifier : quelle est la période du générateur ? ie combien de nombres différents peut il générer ? RNG32 manipule des entiers 32bits, donc au mieux il pourra générer \( 2^{32} \) nombres, soit plus de 4 milliards, ça parait beaucoup, mais est-ce suffisant pour calculer une image ?
pour une petite image, 1024x1024 par exemple, il y a \( 2^{10} \cdot 2^{10} = 2^{20} \) pixels, ce qui ne laisse que \(2^{12} = 4096\) nombres aléatoires pour calculer chaque pixel. pour une image "normale", 2000x1000, il y a 2 fois plus de pixels et donc environ 2000 nombres aléatoires par pixel. ce sera sans doute un peu juste lorsque l'on calcule plusieurs rebonds comme dans Monte Carlo et éclairage indirect...
on peut utiliser le générateur 64bits avec une plus grosse période cf RNG64
par exemple, les constantes se trouvent également sur wikipedia.
attention : pour changer la période du générateur, il ne suffit pas de faire les calculs sur des entiers 64bits, ie les constantes du générateur doivent être différentes aussi. l'exemple de code au dessus utilise des paramètres templates pour générer les 2 versions : ie les calculs sont exactement les mêmes, seul le type des entiers et les constantes changent.
on vient de voir une première version qui permet de rendre tout le calcul de l'image déterministe, ie l'image calculée ne dépend plus que de l'initialisation du générateur, cf la variable seed. mais cette solution n'est pas très statisfaisante, on aimerait quelque chose de plus souple : on n'est pas du tout sur d'utiliser le même nombre de valeurs aléatoires par pixel...
Lorsque la période du générateur est importante, on peut sélectionner l'indice de départ au hasard au lieu de le calculer en fonction de l'indice du pixel. le risque de collision est très faible (ie il y a peu de chances que 2 pixels utilisent le même point de départ et les mêmes nombres aléatoires)... et comme changer le point de départ d'un LCG revient à changer son état interne... c'est strictement équivalent à choisir une initialisation différente par pixel, au hasard. mais on a toujours besoin d'un nombre aléatoire par pixel et il faut obtenir ce nombre aléatoire en parallèle. on revient au point de départ...
Les générateurs LCG que l'on vient rapidement de voir font parti des plus simples et des plus vieux. une amélioration est apparue récemment : PCG pour permuted congruential generator, c'est un LCG avec une permutation en plus... les nombres aléatoires sont produits en 2 étapes, un LCG classique pour commencer, suivi d'une fonction de hachage... et c'est tout ! l'utilisation de la fonction de hachage rend les résultats plus aléatoires, PCG est de meilleure qualité qu'un LCG. mais PCG introduit aussi une fonctionnalité intéressante : on peut générer plusieurs séquences de nombres aléatoires au lieu d'une seule... et on peut générer beaucoup de séquences différentes, une par pixel par exemple !
comment ça marche ? en reprenant la notation avec la fonction \( f \), on peut décrire PCG :
\[ s_i= f(s_{i-1}) = as_{i-1} + c \mod m\\ x_i= g( s_i )\\ \mbox{avec } s_0 = \mbox{seed}\\ \mbox{avec } c = \mbox{stream}\\ \]
la nouvelle fonction \( g \) est la fonction résultat qui construit le nombre aléatoire à partir de \( s \) l'état interne du générateur, et \( f \) produit un nouvel état à partir de l'ancien, comme pour les LCG. \( g \) est une fonction de hachage qui permute les bits du résultat / la représentation binaire du nombre aléatoire.
pour obtenir plusieurs séquences de nombres aléatoires, il suffit de modifier la constante \( c \). la constante \( a \) ne change pas, elle est responsable de la qualité des nombres aléatoires générés et sa valeur est le résultat d'une longue optimisation.
remarque : tous les LCG peuvent générer des séquences différentes en modifiant leur constante c, mais PCG a été optimisé et bien testé, cf l'annexe tester un générateur.
PCG32 est décrit en détail sur le site pcg-random.org, le code officiel est sur github, en C et C++. mais on peut écrire une version très compacte de PCG32 :
attention : le générateur s'appelle pcg 32 mais il a bien une période suffisante de \( 2^{64} \) et peut générer \( 2^{63} \) séquences différentes.
il suffit maintenant d'initialiser un générateur et une séquence par pixel, et pour initialiser la séquence, on peut tout simplement utiliser l'indice du pixel !!
et voila !! on a même plus vraiment besoin de la fonction index()
puisque chaque pixel utilise une séquence différente.
l'histoire de la conception des générateurs de nombres aléatoires est assez longue... et les générateurs inclus dans la librairie standard ne sont pas les meilleurs, ni en terme de vitesse, ni en terme de qualité ou en fonctionnalités.
pour les curieux : "History of Random Number Generators" P. L'Ecuyer 2017 slides.
pour les curieux : "Random Numbers for Parallel Computers: Requirements and Methods, With Emphasis on GPUs" P. L'Ecuyer 2017
Il existe de nombreuses manières de produire des nombres aléatoires, comme présenté par Pierre L'Ecuyer dans "History of Random Number Generators" (cf la présentation juste au dessus)
une de mes préférée utilise uniquement une fonction de hachage. Plusieurs générateurs de ce type sont présentés dans "Parallel random numbers: as easy as 1, 2, 3" Salmon 2011. l'idée est d'utiliser les travaux sur les fonctions de chiffrement pour transformer un compteur, ie la séquence 0, 1, 2, 3, etc en séquence aléatoire ! en reprenant les fonctions \( f \) et \( g \) :
\[ s_i= f(s_{i-1}) = s_{i-1} + 1\\ x_i= g( s_i ) \]
la fonction de transition \( f \) ne fait rien, c'est juste un compteur, c'est la fonction résultat \( g \) qui transforme \( i \) directement, tout le contraire d'un LCG, ou tout le travail est fait par \( f \).
les fonctions de chiffrement utilisés par l'article original sont assez complexes, lentes et nécessitent de stocker plusieurs entiers. par contre, elles produisent des nombres aléatoires parfaitement uniformes ! ces fonctions sont toutes basées sur le même principe : une fonction simple appliquée plusieurs fois.
par exemple, un générateur simple comme Philox 32bits, utilise une fonction de base, un round qui multiplie le compteur par une constante mais renvoie un résultat dont les bits de poids forts sont modifiés par un seed / une clé :
et le générateur complet itère 10 fois cette fonction (tout en modifiant la clé à chaque fois) pour produire un résultat aléatoire :
cette version de Philox passe les tests de qualité à partir de 6 itérations de la fonction de chiffrement. les auteurs préferrent utiliser 10 itérations pour éviter les surprises.
on peut également construire un générateur de nombres aléatoires en utilisant des fonctions de hachage de bonne qualité, ie avec une bonne propriété d'avalanche, comme celles optimisées par le projet hash-propector sur github. cette propriété garanti que si un bit de l'entrée change, la moitiée des bits du résultat a changé aussi ! c'est ce qui permet de transformer une séquence de valeurs consécutives, 0, 1, 2, 3 etc en valeurs aléatoires, ie au moins la moitiée des bits de hash(1)
et de hash(2)
sont différents.
ces fonctions sont construites en itérant plusieurs fois une fonction de base :
par exemple, cette fonction avec 2 itérations :
permet de construire directement un générateur 32 bits basé compteur de meilleure qualité que les générateurs LCG de la librairie standard :
pour les curieux : vérifier la qualité de ce générateur avec practrand comme expliqué dans l'annexe juste après.
et la version 64bits est de bien meilleure qualité, presque aussi bonne que PCG32 :
pour calculer une image, il est direct d'utiliser la fonction index(i)
pour chaque pixel, ou d'initialiser un générateur par pixel avec l'indice du pixel dans l'image : CRNG rng( seed * pi )
le plus direct est d'utiliser CRNG32, ie la fonction de hachage 32 bits juste au dessus, mais attention la période n'est que \( 2^{32} \).
il existe aussi une version de PCG32 qui ne manipule que des entiers 32 bits, cf PCG32I, la période de chaque séquence est également \( 2^{32} \) mais il y a \( 2^{31} \) séquences différentes, ce qui suffit largement pour calculer une image en utilisant une séquence par pixel.
practrand est une batterie de tests statistiques qui permet d'évaluer la qualité d'un générateur de nombre pseudo aléatoire, il est assez simple de l'utiliser. il suffit de récupérer l'archive de sources actuelle, cf https://pracrand.sourceforge.net/ , et de compiler l'utilitaire RNG_test :
la manière la plus simple d'utiliser RNG_test est d'écrire la séquence de nombres aléatoires à tester sur la sortie standard et d'utiliser un pipe vers RNG_test. on a besoin d'un programme de test pour générer les nombres aléatoires et les écrire sur la sortie standard, par exemple :
et on peut tester la qualité du LCG de base :
les résultats ne sont pas très rassurants :
les LCG de la librairie standard ne font pas mieux, vérifiez !
pour PCG32, il faudra être un peu plus patient :
les différents générateurs sont disponibles dans gKit :
index(i)
,remarque : ces codes sont repris / restructurés à partir des codes officiels. rand123.h propose des implementations très compactes et lisibles du code officiel. les tests statistiques laissent penser que je n'ai pas fait d'erreur grossière en manipulant le code, mais ce ne sont pas les implementations officielles.