Aller au contenu

5 · Finance et cryptographie

Deux domaines aux profils opposés : l'un massivement parallèle et adapté, l'autre en tension permanente entre parallélisme et séquentialité.


5.1 Monte-Carlo : le cas idéal

La valorisation d'un produit dérivé par simulation de Monte-Carlo estime :

\[ V = e^{-rT} \, \mathbb{E}\!\left[ \Phi(S_T) \right] \approx \frac{e^{-rT}}{M} \sum_{m=1}^{M} \Phi\!\left(S_T^{(m)}\right) \]

où \(M\) est le nombre de trajectoires simulées, \(S_T^{(m)}\) le prix du sous-jacent à l'échéance dans la trajectoire \(m\), \(\Phi\) la fonction de paiement, \(r\) le taux sans risque et \(T\) la maturité.

Les \(M\) trajectoires sont indépendantes. C'est le parallélisme parfait.

#include <curand_kernel.h>

__global__ void monte_carlo(float* resultats, int n_traj,
                            float S0, float K, float r,
                            float sigma, float T, unsigned graine) {
    int tid = blockIdx.x * blockDim.x + threadIdx.x;
    if (tid >= n_traj) return;

    curandState etat;
    curand_init(graine, tid, 0, &etat);        // sous-séquence par thread

    float z = curand_normal(&etat);
    // Schéma de Black-Scholes exact en une étape
    float ST = S0 * expf((r - 0.5f*sigma*sigma)*T + sigma*sqrtf(T)*z);
    resultats[tid] = fmaxf(ST - K, 0.0f);      // call européen
}
// puis réduction sur resultats, actualisation par exp(-rT)

Ordres de grandeur constatés : 20× à 100× contre un CPU multithreadé, et une valorisation qui prenait des minutes devient interactive.

Les trois points d'attention

1. La génération de nombres aléatoires.

curand_init avec des sous-séquences distinctes par thread est coûteux (plusieurs milliers de cycles pour l'initialisation de MRG32k3a ou XORWOW). Deux réponses :

  • pré-générer les nombres aléatoires dans un tableau (curandGenerateNormal), puis les lire — le noyau devient limité par la mémoire mais l'initialisation disparaît ;
  • utiliser Philox (curand_init avec curandStatePhilox4_32_10_t), un générateur sans état dont l'initialisation est quasi gratuite.

Le choix du générateur importe : il faut garantir que les sous-séquences des threads ne se recouvrent pas, faute de quoi les trajectoires sont corrélées et l'estimateur est biaisé.

2. Les suites à discrépance faible.

Sobol et Halton convergent en \(O(1/M)\) au lieu de \(O(1/\sqrt{M})\) pour le Monte-Carlo classique. cuRAND fournit des générateurs Sobol scramblés. Le gain est considérable : atteindre la même précision avec 100 fois moins de trajectoires.

3. Les options américaines.

Le paiement dépend d'une décision d'exercice optimal à chaque date, ce qui introduit une récursion arrière. L'algorithme de Longstaff-Schwartz procède par régression sur les trajectoires simulées : à chaque date, on régresse la valeur de continuation sur des fonctions de base.

C'est parallélisable par date, mais les dates sont séquentielles. Le parallélisme disponible est celui des trajectoires à date fixe — encore confortable, mais la régression (une résolution de moindres carrés) doit être faite entre chaque date.


5.2 Le calcul de risque

VaR, CVaR, stress tests : simuler des milliers de scénarios de marché et revaloriser tout un portefeuille dans chacun.

Structure : \(S\) scénarios × \(P\) positions = \(S \times P\) valorisations indépendantes. Pour \(S = 10^4\) et \(P = 10^5\), cela fait \(10^9\) valorisations.

C'est massivement parallèle, mais avec une difficulté : les positions sont hétérogènes. Une obligation, une option, un swap n'utilisent pas le même modèle de valorisation.

Deux stratégies :

Stratégie Principe Compromis
Un noyau par type d'instrument trier les positions par type, un lancement par type pas de divergence, mais beaucoup de petits noyaux
Un noyau avec dispatch un switch sur le type divergence maximale dans le warp

La première est presque toujours la bonne : c'est du tri suivi de noyaux homogènes, et cela évite la divergence.

Le lien avec les megakernels

Beaucoup de petits noyaux hétérogènes qui s'enchaînent, avec un coût de lancement significatif : c'est exactement la structure que les megakernels attaquent en IA.

À la connaissance de ce document, personne n'a publié de megakernel pour le calcul de risque financier. C'est une piste ouverte : l'analogie structurelle est forte, et le régime (latence critique, beaucoup de petits noyaux) est le même.

La sensibilité aux paramètres (les « grecques »)

Calculer \(\partial V / \partial S\), \(\partial V / \partial \sigma\), etc.

Trois méthodes :

  1. Différences finies — simple, \(2n\) valorisations pour \(n\) sensibilités, et numériquement fragile ;
  2. Pathwise / likelihood ratio — analytique, exact, mais spécifique au produit ;
  3. Différentiation automatique en mode adjoint (AAD) — coût constant quel que soit le nombre de sensibilités.

L'AAD est la méthode de référence en finance quantitative moderne, et elle s'implémente sur GPU (avec les mêmes difficultés que la rétropropagation : stockage de la bande d'exécution, gestion mémoire).


5.3 Le trading haute fréquence

Le GPU est généralement le mauvais outil

Le HFT optimise la latence de bout en bout : de la réception d'un paquet réseau à l'émission d'un ordre. Les budgets sont de l'ordre de la microseconde, parfois moins.

Or un aller-retour GPU coûte :

  • transfert PCIe : ~2 à 10 µs ;
  • lancement de noyau : ~5 µs ;
  • retour : ~2 à 10 µs.

Le GPU est disqualifié avant même de calculer quoi que ce soit.

Ce qui est utilisé à la place : des FPGA, qui traitent le paquet réseau au fil de l'eau avec une latence de quelques centaines de nanosecondes, et des CPU avec contournement du noyau (DPDK, Solarflare Onload).

Le GPU a sa place en amont :

  • calibration de modèles ;
  • backtesting massivement parallèle ;
  • entraînement de modèles prédictifs ;
  • calcul de risque intrajournalier.

C'est-à-dire partout où le débit compte plus que la latence.


5.4 La cryptographie

Un domaine en tension structurelle.

Ce qui marche bien

Les attaques par force brute et le cassage de mots de passe. Hashcat et John the Ripper exploitent le GPU pour tester des milliards de candidats par seconde : chaque candidat est indépendant, c'est du parallélisme pur.

C'est d'ailleurs la raison d'être des fonctions de dérivation de clé résistantes au GPU : bcrypt, scrypt et Argon2 sont conçues pour être coûteuses en mémoire et en accès irréguliers, précisément pour annuler l'avantage du GPU.

Les preuves à divulgation nulle de connaissance (zero-knowledge proofs). Les schémas zk-SNARK et zk-STARK reposent sur :

  • des multi-scalar multiplications (MSM) sur courbes elliptiques ;
  • des NTT (transformées de Fourier sur corps finis).

Les deux sont massivement parallèles, et le GPU y apporte des accélérations d'un ordre de grandeur. C'est aujourd'hui un domaine actif d'ingénierie GPU.

Le chiffrement homomorphe. Les schémas BFV, CKKS, TFHE manipulent de très grands polynômes avec des NTT. Le GPU y est bien adapté, et c'est nécessaire : sans accélération, ces schémas sont trop lents pour tout usage pratique.

Ce qui marche mal

Le chiffrement par blocs en mode chaîné. AES-CBC chiffre le bloc \(n\) en fonction du bloc \(n-1\) : c'est séquentiel par construction. Les modes parallélisables (CTR, GCM, XTS) n'ont pas ce problème.

Mais même là, le GPU n'a pas d'avantage décisif : les CPU modernes ont des instructions AES-NI qui chiffrent à plusieurs gigaoctets par seconde par cœur. Le gain GPU est marginal une fois le transfert PCIe pris en compte.

Les fonctions de hachage sur de petites entrées. Hacher un message de 64 octets ne donne aucun parallélisme interne. Le GPU ne gagne que s'il y a des millions de messages indépendants.

Sur le minage de cryptomonnaies

C'est historiquement un usage majeur du GPU. Deux évolutions l'ont réduit : le passage d'Ethereum en preuve d'enjeu (2022) a supprimé le plus gros marché GPU, et le minage Bitcoin est dominé par des ASIC depuis 2013, contre lesquels le GPU n'a aucune chance en efficacité énergétique.

Ce document ne traite pas ce sujet, qui relève davantage de l'économie du matériel que de la programmation GPU.


Résumé du chapitre

À retenir

  • Monte-Carlo est le cas GPU idéal : trajectoires indépendantes, 20 à 100× d'accélération.
  • Attention à la génération aléatoire : curand_init est coûteux, préférez Philox (sans état) ou la pré-génération. Vérifiez la non-superposition des sous-séquences.
  • Les suites à discrépance faible (Sobol) convergent en \(O(1/M)\) au lieu de \(O(1/\sqrt{M})\).
  • Pour le risque sur portefeuille hétérogène : trier par type d'instrument et lancer un noyau homogène par type.
  • Le HFT n'est pas un cas GPU : le budget de latence est inférieur au seul aller-retour PCIe. Ce sont les FPGA qui occupent ce terrain.
  • En cryptographie, le GPU excelle sur les zk-proofs (MSM, NTT), le chiffrement homomorphe et la force brute ; il n'apporte rien sur le chiffrement symétrique (AES-NI côté CPU) ni sur les modes chaînés.

Vérifiez que vous avez compris

Pourquoi curand_init dans le noyau est-il souvent une mauvaise idée ?

Parce que l'initialisation d'un état XORWOW ou MRG32k3a avec une sous-séquence distincte par thread coûte plusieurs milliers de cycles — largement plus que la génération elle-même si vous ne tirez que quelques nombres par thread.

Deux remèdes :

  1. Philox (curandStatePhilox4_32_10_t), un générateur à compteur dont l'état se calcule directement depuis (graine, compteur) : initialisation quasi gratuite.
  2. Pré-génération avec l'API hôte (curandGenerateNormal) dans un tableau, que le noyau lit ensuite. Le noyau devient limité par la mémoire, ce qui est souvent acceptable.
Vous valorisez 100 000 positions de 12 types différents. Un noyau ou douze ?

Douze. Le raisonnement :

Avec un noyau unique et un switch sur le type, les 32 threads d'un warp tombent sur des types différents. Le matériel exécute les 12 branches successivement pour chaque warp : efficacité \(\approx 1/12\).

Avec un tri préalable par type (un cub::DeviceRadixSort sur le champ type, quelques millisecondes) puis douze lancements homogènes, chaque warp exécute un seul chemin : aucune divergence.

Le coût du tri et des lancements supplémentaires (12 × 5 µs = 60 µs) est négligeable devant le gain.

Pourquoi Argon2 est-il « résistant au GPU », alors que le GPU est censé être bon en force brute ?

Parce qu'Argon2 est délibérément conçu pour être coûteux en mémoire et irrégulier dans ses accès.

Il alloue un grand tampon (paramétrable, typiquement 64 Mo à 1 Go) et y fait des accès dépendant des données, en séquence.

Sur GPU, cela produit trois obstacles :

  1. la mémoire partagée d'un SM (227 Ko) est très insuffisante — il faut aller en HBM ;
  2. le nombre de tentatives concurrentes est limité par la VRAM divisée par le coût mémoire de chacune : un GPU de 80 Go ne peut mener que 80 tentatives simultanées à 1 Go chacune ;
  3. les accès sont irréguliers et dépendants, donc non coalescés et non recouvrables.

Le GPU perd son avantage de parallélisme parce que la ressource limitante devient la mémoire, où son avance sur le CPU est bien plus faible que sur le calcul. C'est une belle application inversée du roofline.


Chapitre suivant : 6 · Ce qui ne va pas sur GPU


Sources de ce chapitre