Programmation GPU idiomatique dans Rust: Emu Library


Présentation


Emu est un langage de programmation de carte graphique de haut niveau qui peut être intégré dans du code normal dans le langage de programmation du système Rust .


Cet article se concentrera sur la syntaxe d'Emu, ses fonctionnalités et plusieurs exemples illustratifs de son utilisation dans du code réel seront présentés.


L'installation


  1. La bibliothèque que vous recherchez a besoin d'une dépendance OpenCL externe. Vous devez installer le pilote approprié à votre matériel.
  2. Cargo.toml texte ci-dessous. Cela téléchargera les dernières versions disponibles (si vous avez besoin d'un assemblage spécifique, alors au lieu de * mettez la version dont vous avez besoin):

     [dependencies] em = "*" //   Emu ocl = "*" //   OpenCL 

Syntaxe


La syntaxe Emu est assez simple, car ce langage est uniquement destiné à l'écriture des fonctions du noyau qui sont compilées dans OpenCL .


Types de données


La langue Emu possède neuf types de données similaires à ceux de Rust. Voici un tableau de ces types de données:


Le titreLa description
f32Nombre Ă  virgule flottante de trente-deux bits
i8Symbole ou numéro à huit bits
i16Numéro de seize bits signé
i32Numéro de trente-deux bits signé
i64Numéro de soixante-quatre bits signé
u8Numéro à huit bits non signé
u16Numéro de seize bits non signé
u32Numéro de trente-deux bits non signé
u64Numéro 64 bits non signé
boolValeur booléenne
[TYPE]Un vecteur composé de variables de type TYPE

Variables


Les variables sont déclarées à l'aide du mot clé let , situé derrière l'identifiant, le signe deux-points, le type de données, le signe égal, la valeur affectée et le point-virgule.


 let age: i32 = 54; let growth: f32 = 179.432; let married: bool = true; 

Conversions


La conversion des types de données primitifs s'effectue à l'aide de l'opérateur binaire as , à la suite du type cible. Je note que le type cible peut également être une unité de mesure (voir la section suivante):


 let width: i16 = 324; let converted_width: i64 = width as i64; 

Unités


La langue Emu vous permet de traiter les nombres comme des unités de mesure, ce qui est conçu pour simplifier les calculs scientifiques. Dans cet exemple, la length variable length initialement définie en mètres, mais ensuite d'autres unités de mesure y sont ajoutées:


 let length: f32 = 3455.345; //  length += 7644.30405 as cm; //  length += 1687.3043 as mm; //  

Constantes prédéfinies


Emu possède un ensemble de constantes prédéfinies qui sont pratiques à utiliser dans la pratique. Voici le tableau correspondant.


Le titreValeur
Y10 Ă  la puissance de 24
Z10 Ă  la puissance de 21
E10 Ă  la puissance de 18
P10 Ă  la puissance de 15
T10 Ă  la puissance de 12
G10 Ă  la puissance de 9
M10 Ă  la puissance de 6
k10 Ă  la puissance de 3
h10 Ă  la puissance de 2
D10 Ă  la puissance de 1
d10 Ă  la puissance de -1
c10 Ă  la puissance de -2
m10 Ă  la puissance de -3
u10 Ă  la puissance de -6
n10 Ă  la puissance de -9
p10 Ă  la puissance de -12
f10 Ă  la puissance de -15
a10 Ă  -18
z10 Ă  la puissance de -21
y10 Ă  la puissance de -24

Des constantes correspondant à des données scientifiques sont également définies. Vous pouvez trouver le tableau composé de ces constantes ici .


Déclarations conditionnelles


Les instructions conditionnelles Emu sont similaires aux instructions correspondantes dans Rust. Le code suivant utilise des constructions conditionnelles:


 let number: i32 = 2634; let satisfied: bool = false; if (number > 0) && (number % 2 == 0) { satisfied = true; } 

Pour les boucles


L'en-tête de la boucle For est défini comme for NUM in START..END , où NUM est une variable qui prend des valeurs de la plage [START; END) [START; END) par l'unité.


 let sum: u64 = 0; for i in 0..215 { sum += i; } 

Boucles


Le titre de la boucle While est défini comme while (CONDITION) , où CONDITION est la condition pour que la boucle passe à l'itération suivante. Ce code est similaire à l'exemple précédent:


 let sum: u64 = 0; let idx: i32 = 0; while (idx < 215) { sum += idx; idx += 1; } 

Boucles sans fin


Les boucles infinies n'ont pas de condition de sortie explicite et sont définies par le mot clé loop . Cependant, elles peuvent être poursuivies ou interrompues par les instructions break et continue (comme les deux autres types de boucles).


 let collapsed: u64 = 1; let idx: i32 = 0; loop { if idx % 2 == 0 { continue; } sum *= idx; if idx == 12 { break; } } 

Retour de fonction


Comme dans tous les autres langages de programmation, l' return est la sortie de la fonction courante. Il peut également retourner une certaine valeur si la signature de fonction (voir les sections suivantes) le permet.


 let result: i32 = 23446; return result; 

Autres opérateurs


  • OpĂ©rateurs d'affectation disponibles: = , += , -= , *= , /= , %= , &= , ^= , <<= , >>= ;
  • L'opĂ©rateur d'index est [IDX] ;
  • OpĂ©rateur d'appel - (ARGS) ;
  • OpĂ©rateurs unaires: * pour le dĂ©rĂ©fĂ©rencement,! inverser les donnĂ©es boolĂ©ennes, - annuler les nombres;
  • OpĂ©rateurs binaires: + , - , * , / , % , && , || , & , | , ^ , >> , << , > , < , >= , <= , == != .

Les fonctions


Il y a trois parties de fonctions sur Emu: l'identifiant, les paramètres et le corps de la fonction, consistant en une séquence d'instructions exécutables. Considérez la fonction d'ajouter deux nombres:


 add(left f32, right f32) f32 { return left + right; } 

Comme vous l'avez peut-être remarqué, cette fonction renvoie la somme de deux arguments qui lui sont passés à l'aide du type de données f32 .


Espaces d'adresses


Chaque paramètre de la fonction correspond à un espace d'adressage spécifique . Par défaut, tous les paramètres correspondent à l'espace __private__ .


L'ajout des préfixes global_ et local_ à l'identificateur de paramètre indique explicitement son espace d'adressage.


La documentation conseille d'utiliser le préfixe global_ pour tous les vecteurs et de ne pas préfixer autre chose.


Fonctions intégrées


Emu fournit un petit ensemble de fonctions intégrées (empruntées à OpenCL) qui vous permettent de gérer les données GPU:


  • get_work_dim() - Retourne le nombre de dimensions;
  • get_global_size() - Retourne le nombre d'Ă©lĂ©ments globaux pour une dimension donnĂ©e;
  • get_global_id() - Retourne l'identifiant unique de l'Ă©lĂ©ment pour la dimension spĂ©cifiĂ©e;
  • get_global_size() - Retourne le nombre d'Ă©lĂ©ments globaux pour une dimension donnĂ©e;
  • get_local_id() - Retourne un identifiant unique pour un Ă©lĂ©ment local dans un groupe de travail spĂ©cifique pour une dimension donnĂ©e;
  • get_num_groups() - Retourne le nombre de groupes de travail pour une dimension donnĂ©e;
  • get_group_id() - Retourne un identifiant unique pour le groupe de travail.

Dans le code d'application, vous trouverez le plus souvent l'expression get_global_id(0) , qui retourne l'index actuel de l'élément vectoriel associé à l'appel à votre fonction noyau.


Exécution de code


Considérez la syntaxe pour appeler les fonctions Emu à partir du code Rust standard. À titre d'exemple, nous utiliserons une fonction qui multiplie tous les éléments d'un vecteur par un nombre donné:


 use em::emu; emu! { multiply(global_vector [f32], scalar f32) { global_vector[get_global_id(0)] *= scalar; } } 

Pour traduire cette fonction en code OpenCL, vous devez mettre sa signature dans la macro de build! comme suit:


 use em::build; //    build! {...} extern crate ocl; use ocl::{flags, Platform, Device, Context, Queue, Program, Buffer, Kernel}; build! { multiply [f32] f32 } 

D'autres actions se résument à appeler les fonctions Emu que vous avez écrites à partir du code Rust. Rien de plus simple:


 fn main() { let vector = vec![0.4445, 433.245, 87.539503, 2.0]; let result = multiply(vector, 2.0).unwrap(); dbg!(result); } 

Exemple d'application


Ce programme prend un scalaire comme premier argument, par lequel il est nécessaire de multiplier les arguments suivants. Le vecteur résultant sera imprimé sur la console:


 use em::{build, emu}; //    build! {...} extern crate ocl; use ocl::{flags, Buffer, Context, Device, Kernel, Platform, Program, Queue}; emu! { multiply(global_vector [f32], scalar f32) { global_vector[get_global_id(0)] *= scalar; } } build! { multiply [f32] f32 } fn main() { //     : let args = std::env::args().collect::<Vec<String>>(); if args.len() < 3 { panic!(": cargo run -- <SCALAR> <NUMBERS>..."); } //      : let scalar = args[1].parse::<f32>().unwrap(); //      : let vector = args[2..] .into_iter() .map(|string| string.parse::<f32>().unwrap()) .collect(); //    : let result = multiply(vector, scalar).unwrap(); dbg!(result); } 

Vous pouvez exécuter ce code avec la commande cargo run -- 3 2.1 3.6 6.2 . La conclusion qui en résulte répond aux attentes:


 [src/main.rs:33] result = [ 6.2999997, 10.799999, 18.599998, ] 

Lien vers OpenCL


Comme mentionné précédemment, Emu n'est qu'une abstraction sur OpenCL , et donc il a la capacité d'interagir avec la caisse ocl. Le code ci-dessous est tiré d'un exemple dans le référentiel officiel :


 use em::emu; //  "ocl"        Rust: extern crate ocl; use ocl::{flags, Platform, Device, Context, Queue, Program, Buffer, Kernel}; //  Emu    (OpenCL)   //     "EMU: &'static str": emu! { //     : multiply(global_buffer [f32], coeff f32) { global_buffer[get_global_id(0)] *= coeff; } } fn multiply(global_buffer: Vec<f32>, coeff: f32) -> ocl::Result<Vec<f32>> { //        , //  , ,   : let platform = Platform::default(); let device = Device::first(platform)?; let context = Context::builder() .platform(platform) .devices(device.clone()) .build()?; let program = Program::builder() .devices(device) .src(EMU) .build(&context)?; let queue = Queue::new(&context, device, None)?; let dims = global_buffer.len(); //    : let buffer = Buffer::<f32>::builder() .queue(queue.clone()) .flags(flags::MEM_READ_WRITE) .len(dims) .copy_host_slice(&global_buffer) .build()?; //       , //    : let kernel = Kernel::builder() .program(&program) .name("multiply") .queue(queue.clone()) .global_work_size(dims) .arg(&buffer) .arg(&coeff) .build()?; //    (    //   : unsafe { kernel.cmd() .queue(&queue) .global_work_offset(kernel.default_global_work_offset()) .global_work_size([dims, 0, 0]) .local_work_size(kernel.default_local_work_size()) .enq()?; } //  ,         // "dims": let mut vector = vec![0.0f32; dims]; buffer.cmd() .queue(&queue) .offset(0) .read(&mut vector) .enq()?; Ok(vector) } fn main() { let initial_data = vec![3.7, 4.5, 9.0, 1.2, 8.9]; //   ,   Emu,  //  "initial_data": let final_data = multiply(initial_data, 3.0).unwrap(); println!("{:?}", final_data); } 

Achèvement


J'espère que l'article vous a plu. Vous pouvez obtenir une réponse rapide à vos questions dans le chat en russe à Rust ( version pour débutants ).


Source: https://habr.com/ru/post/fr454678/


All Articles