Programación de GPU idiomática en Rust: Emu Library


Introduccion


Emu es un lenguaje de programación de tarjetas gráficas de alto nivel que puede integrarse en un código normal en el lenguaje de programación del sistema Rust .


Este artículo se centrará en la sintaxis de Emu, sus características, y también mostrará algunos ejemplos ilustrativos de su uso en código real.


Instalación


  1. La biblioteca que está buscando necesita una dependencia externa de OpenCL. Necesita instalar el controlador apropiado para su hardware.
  2. Cargo.toml texto a continuación. Esto descargará las últimas versiones disponibles (si necesita un ensamblaje específico, en lugar de * ponga la versión que necesita):

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

Sintaxis


La sintaxis de Emu es bastante simple, porque este lenguaje está destinado solo a escribir funciones del núcleo compiladas en OpenCL .


Tipos de datos


El lenguaje Emu tiene nueve tipos de datos que son similares a los de Rust. La siguiente es una tabla de estos tipos de datos:


TituloDescripción
f32Número de coma flotante de treinta y dos bits
i8Símbolo o número de ocho bits
i16Número de dieciséis bits firmado
i32Número de treinta y dos bits firmado
i64Número de sesenta y cuatro bits firmado
u8Número de ocho bits sin signo
u16Número de dieciséis bits sin signo
u32Número de treinta y dos bits sin signo
u64Número de sesenta y cuatro bits sin signo
boolValor booleano
[TYPE]Un vector que consta de variables de tipo TYPE

Variables


Las variables se declaran usando la palabra clave let , que se encuentra detrás del identificador, dos puntos, tipo de datos, signo igual, valor asignado y punto y coma.


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

Conversiones


La conversión de tipos de datos primitivos se realiza utilizando el operador binario as , siguiendo el tipo de destino. Observo que el tipo de objetivo también puede ser una unidad de medida (consulte la siguiente sección):


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

Unidades


El lenguaje Emu le permite tratar los números como unidades de medida, que está diseñado para simplificar los cálculos científicos. En este ejemplo, la length variable length define inicialmente en metros, pero luego se le agregan otras unidades de medida:


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

Constantes predefinidas


Emu tiene un conjunto de constantes predefinidas que son convenientes de usar en la práctica. A continuación se muestra la tabla correspondiente.


TituloValor
Y10 al poder de 24
Z10 al poder de 21
E10 al poder de 18
P10 a la potencia de 15
T10 al poder de 12
G10 al poder de 9
M10 a la potencia de 6
k10 al poder de 3
h10 al poder de 2
D10 al poder de 1
d10 al poder de -1
c10 al poder de -2
m10 al poder de -3
u10 al poder de -6
n10 al poder de -9
p10 al poder de -12
f10 al poder de -15
a10 al grado de -18
z10 al poder de -21
y10 al poder de -24

También se definen las constantes correspondientes a los datos científicos. Puede encontrar la tabla que consta de estas constantes aquí .


Declaraciones condicionales


Las declaraciones condicionales de Emu son similares a las declaraciones correspondientes en Rust. El siguiente código usa construcciones condicionales:


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

Para bucles


El encabezado del bucle For se define como for NUM in START..END , donde NUM es una variable que toma valores del rango [START; END) [START; END) través de la unidad.


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

Mientras bucles


El título del ciclo While se define como while (CONDITION) , donde CONDITION es la condición para que el ciclo proceda a la siguiente iteración. Este código es similar al ejemplo anterior:


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

Bucles sin fin


Los bucles infinitos no tienen una condición de salida explícita y están definidos por la palabra clave del loop . Sin embargo, pueden continuarse o interrumpirse por las declaraciones break y continue (como los otros dos tipos de bucles).


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

Regresar de la función


Como en todos los demás lenguajes de programación, la return es la salida de la función actual. También puede devolver un cierto valor si la firma de la función (consulte las siguientes secciones) lo permite.


 let result: i32 = 23446; return result; 

Otros operadores


  • Operadores de asignación disponibles: = , += , -= , *= , /= , %= , &= , ^= , <<= , >>= ;
  • El operador de índice es [IDX] ;
  • Operador de llamadas - (ARGS) ;
  • Operadores unarios: * para desreferenciar ,! para invertir datos booleanos, - para negar números;
  • Operadores binarios: + , - , * , / , % , && , || , & , | , ^ , >> , << , > , < , >= , <= , == , == != .

Las funciones


Hay tres partes de funciones en Emu: el identificador, los parámetros y el cuerpo de la función, que consiste en una secuencia de instrucciones ejecutables. Considere la función de sumar dos números:


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

Como habrás notado, esta función devuelve la suma de dos argumentos que se le pasan usando el tipo de datos f32 .


Espacios de direcciones


Cada parámetro de la función corresponde a un espacio de direcciones específico . Por defecto, todos los parámetros corresponden al espacio __private__ .


Agregar los prefijos global_ y global_ al identificador del parámetro indica explícitamente su espacio de direcciones.


La documentación aconseja usar el prefijo global_ para todos los vectores y no agregar nada más.


Funciones incorporadas


Emu proporciona un pequeño conjunto de funciones integradas (tomadas de OpenCL) que le permiten administrar los datos de la GPU:


  • get_work_dim() - Devuelve el número de dimensiones;
  • get_global_size() - Devuelve el número de elementos globales para una dimensión dada;
  • get_global_id() - Devuelve el identificador único del elemento para la dimensión especificada;
  • get_global_size() - Devuelve el número de elementos globales para una dimensión dada;
  • get_local_id() - Devuelve un identificador único para un elemento local dentro de un grupo de trabajo específico para una dimensión dada;
  • get_num_groups() - Devuelve el número de grupos de trabajo para una dimensión dada;
  • get_group_id() - Devuelve un identificador único para el grupo de trabajo.

En el código de la aplicación, con mayor frecuencia encontrará la expresión get_global_id(0) , que devuelve el índice actual del elemento vector asociado con la llamada a la función de su núcleo.


Ejecución de código


Considere la sintaxis para llamar a las funciones Emu desde el código Rust normal. Como ejemplo, utilizaremos una función que multiplique todos los elementos de un vector por un número dado:


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

Para traducir esta función en código OpenCL, ¡debe poner su firma en la macro de build! como sigue:


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

Otras acciones se reducen a llamar a las funciones de Emu que escribió desde el código Rust. No podría ser más fácil:


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

Ejemplo de aplicación


Este programa toma un escalar como primer argumento, por el cual es necesario multiplicar los siguientes argumentos. El vector resultante se imprimirá en la consola:


 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); } 

Puede ejecutar este código con el comando cargo run -- 3 2.1 3.6 6.2 . La conclusión resultante cumple con las expectativas:


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

Enlace a OpenCL


Como se mencionó anteriormente, Emu es solo una abstracción sobre OpenCL y, por lo tanto, tiene la capacidad de interactuar con la caja . El siguiente código está tomado de un ejemplo en el repositorio oficial :


 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); } 

Finalización


Espero que hayas disfrutado el artículo. Puede obtener una respuesta rápida a sus preguntas en el chat de idioma ruso en Rust ( versión para principiantes ).


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


All Articles