Programação de GPU Idiomatic em Rust: Emu Library


1. Introdução


O UEM é uma linguagem de programação de placa gráfica de alto nível que pode ser incorporada ao código regular na linguagem de programação do sistema Rust .


Este artigo abordará a sintaxe do Emu, seus recursos e também mostrará alguns exemplos ilustrativos de seu uso em código real.


Instalação


  1. A biblioteca revisada precisa de uma dependência externa do OpenCL. Você precisa instalar o driver apropriado para o seu hardware.
  2. Cargo.toml texto abaixo. Isso fará o download das versões disponíveis mais recentes (se você precisar de um assembly específico, em vez de * coloque a versão necessária):

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

Sintaxe


A sintaxe da Emu é bastante simples, porque esse idioma é destinado apenas para escrever funções do kernel que são transmitidas no OpenCL durante a compilação.


Tipos de dados


A linguagem Emu possui nove tipos de dados semelhantes aos do Rust. A seguir, é apresentada uma tabela desses tipos de dados:


TítuloDescrição do produto
f32Número de ponto flutuante de trinta e dois bits
i8Símbolo ou número de oito bits
i16Número de dezesseis bits assinado
i32Número de trinta e dois bits assinado
i64Número de sessenta e quatro bits assinado
u8Número de oito bits não assinado
u16Número de dezesseis bits não assinado
u32Número de trinta e dois bits não assinado
u64Número de sessenta e quatro bits não assinado
boolValor booleano
[TYPE]Um vetor que consiste em variáveis ​​do tipo TYPE

Variáveis


As variáveis ​​são declaradas usando a palavra-chave let , localizada atrás do identificador, dois pontos, tipo de dados, sinal de igual, valor atribuído e ponto e vírgula.


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

Conversões


A conversão de tipos de dados primitivos é realizada usando o operador binário as , seguindo o tipo de destino. Observo que o tipo de destino também pode ser uma unidade de medida (consulte a próxima seção):


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

Unidades


A linguagem Emu permite tratar números como unidades de medida, projetadas para simplificar cálculos científicos. Neste exemplo, o length variável length definido inicialmente em metros, mas outras unidades de medida são adicionadas a ele:


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

Constantes predefinidas


A UEM possui um conjunto de constantes predefinidas que são convenientes para uso na prática. Abaixo está a tabela correspondente.


TítuloValor
Y10 ao poder de 24
Z10 ao poder de 21
E10 ao poder de 18
P10 à potência de 15
T10 à potência de 12
G10 ao poder de 9
M10 ao poder de 6
k10 ao poder de 3
h10 à potência de 2
D10 ao poder de 1
d10 à potência de -1
c10 à potência de -2
m10 à potência de -3
u10 ao poder de -6
n10 ao poder de -9
p10 à potência de -12
f10 à potência de -15
a10 ao grau de -18
z10 ao poder de -21
y10 ao poder de -24

Também são definidas constantes correspondentes a dados científicos. Você pode encontrar a tabela que consiste nessas constantes aqui .


Instruções condicionais


As declarações condicionais da UEM são semelhantes às declarações correspondentes no Rust. O código a seguir usa construções condicionais:


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

Para loops


O cabeçalho do loop For é definido como for NUM in START..END , onde NUM é uma variável que recebe valores do intervalo [START; END) [START; END) através da unidade.


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

Enquanto loops


O título do loop While é definido como while (CONDITION) , em que CONDITION é a condição para o loop prosseguir para a próxima iteração. Este código é semelhante ao exemplo anterior:


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

Loops sem fim


Loops infinitos não têm uma condição de saída explícita e são definidos pela palavra-chave loop . No entanto, eles podem ser continuados ou interrompidos pelas instruções break e continue (como os outros dois tipos de loops).


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

Retorno da função


Como em todas as outras linguagens de programação, a return é a saída da função atual. Também pode retornar um determinado valor se a assinatura da função (consulte as seções a seguir) permitir isso.


 let result: i32 = 23446; return result; 

Outros operadores


  • Operadores de atribuição disponíveis: = , += , -= , *= , /= , %= , &= , ^= , <<= , >>= ;
  • O operador de índice é [IDX] ;
  • Operador de chamada - (ARGS) ;
  • Operadores unários: * para cancelamento de referência ,! inverter dados booleanos, - negar números;
  • Operadores binários: + , - , * , / , % , && , || , & , | , ^ , >> , << , > , < , >= , <= , == , == != .

Funções


Existem três partes de funções no Emu: o identificador, parâmetros e o corpo da função, consistindo em uma sequência de instruções executáveis. Considere a função de adicionar dois números:


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

Como você deve ter notado, essa função retorna a soma dos dois argumentos passados ​​para ela usando o tipo de dados f32 .


Espaços de endereço


Cada parâmetro da função corresponde a um espaço de endereço específico . Por padrão, todos os parâmetros correspondem ao espaço __private__ .


Adicionar os prefixos global_ e local_ ao identificador de parâmetro indica explicitamente seu espaço de endereço.


A documentação aconselha o uso do prefixo global_ para todos os vetores e não o prefixo de mais nada.


Funções incorporadas


A UEM fornece um pequeno conjunto de funções internas (extraídas do OpenCL) que permitem gerenciar dados da GPU:


  • get_work_dim() - Retorna o número de dimensões;
  • get_global_size() - Retorna o número de elementos globais para uma determinada dimensão;
  • get_global_id() - Retorna o identificador exclusivo do elemento para a dimensão especificada;
  • get_global_size() - Retorna o número de elementos globais para uma determinada dimensão;
  • get_local_id() - Retorna um identificador exclusivo para um elemento local dentro de um grupo de trabalho específico para uma determinada dimensão;
  • get_num_groups() - Retorna o número de grupos de trabalho para uma determinada dimensão;
  • get_group_id() - Retorna um identificador exclusivo para o grupo de trabalho.

No código do aplicativo, na maioria das vezes você encontrará a expressão get_global_id(0) , que retorna o índice atual do elemento vetorial associado à chamada para sua função do kernel.


Execução de código


Considere a sintaxe para chamar funções de Emu a partir do código Rust regular. Como exemplo, usaremos uma função que multiplica todos os elementos de um vetor por um determinado número:


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

Para converter essa função em código OpenCL, você precisa colocar sua assinatura na macro de build! da seguinte maneira:


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

Outras ações se resumem a chamar funções de Emu que você escreveu a partir do código Rust. Não poderia ser mais fácil:


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

Exemplo de aplicação


Este programa assume um escalar como o primeiro argumento, pelo qual é necessário multiplicar os seguintes argumentos. O vetor resultante será impresso no 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); } 

Você pode executar este código com o comando cargo run -- 3 2.1 3.6 6.2 . A conclusão resultante atende às expectativas:


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

Link para OpenCL


Como mencionado anteriormente, o Emu é apenas uma abstração do OpenCL e, portanto, tem a capacidade de interagir com o oclate. O código abaixo é retirado de um exemplo no repositório 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); } 

Conclusão


Espero que tenham gostado do artigo. Você pode obter uma resposta rápida para suas perguntas no bate-papo em russo no Rust ( versão para iniciantes ).


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


All Articles