Idiomatische GPU-Programmierung in Rust: Emu Library


Einführung


Emu ist eine Programmiersprache für Grafikkarten auf hoher Ebene, die in regulären Code in der Programmiersprache des Rust- Systems eingebettet werden kann.


Dieser Artikel konzentriert sich auf die Syntax von Emu und seine Funktionen und zeigt einige anschauliche Beispiele für die Verwendung in echtem Code.


Installation


  1. Die gesuchte Bibliothek benötigt eine externe OpenCL-Abhängigkeit. Sie müssen den für Ihre Hardware geeigneten Treiber installieren.
  2. Cargo.toml Text. Dadurch werden die neuesten verfügbaren Versionen heruntergeladen (wenn Sie eine bestimmte Baugruppe benötigen, geben Sie anstelle von * gewünschte Version ein):

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

Syntax


Die Emu-Syntax ist recht einfach, da diese Sprache nur zum Schreiben von Kernelfunktionen vorgesehen ist, die in OpenCL kompiliert wurden.


Datentypen


Die Emu-Sprache verfügt über neun Datentypen, die denen in Rust ähnlich sind. Das Folgende ist eine Tabelle dieser Datentypen:


TitelBeschreibung
f3232-Bit-Gleitkommazahl
i8Symbol oder Acht-Bit-Nummer
i16Signierte 16-Bit-Nummer
i32Signierte 32-Bit-Nummer
i64Signierte 64-Bit-Nummer
u8Vorzeichenlose 8-Bit-Nummer
u1616-Bit-Nummer ohne Vorzeichen
u32Zwei-Bit-Nummer ohne Vorzeichen
u64Vorzeichenlose 64-Bit-Nummer
boolBoolescher Wert
[TYPE]Ein Vektor, der aus Variablen vom Typ TYPE

Variablen


Variablen werden mit dem Schlüsselwort let deklariert, das sich hinter dem Bezeichner, dem Doppelpunkt, dem Datentyp, dem Gleichheitszeichen, dem zugewiesenen Wert und dem Semikolon befindet.


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

Konvertierungen


Die Konvertierung primitiver Datentypen erfolgt mit dem Binäroperator as nach dem Zieltyp. Ich stelle fest, dass der Zieltyp auch eine Maßeinheit sein kann (siehe nächster Abschnitt):


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

Einheiten


Mit der Emu-Sprache können Sie Zahlen als Maßeinheiten behandeln, um wissenschaftliche Berechnungen zu vereinfachen. In diesem Beispiel wird die variable length zunächst in Metern definiert, dann werden jedoch andere Maßeinheiten hinzugefügt:


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

Vordefinierte Konstanten


Emu verfügt über eine Reihe vordefinierter Konstanten, die in der Praxis bequem zu verwenden sind. Unten ist die entsprechende Tabelle.


TitelWert
Y10 hoch 24
Z10 hoch 21
E10 hoch 18
P10 hoch 15
T10 hoch 12
G10 hoch 9
M10 hoch 6
k10 hoch 3
h10 hoch 2
D10 hoch 1
d10 hoch -1
c10 hoch -2
m10 hoch -3
u10 hoch -6
n10 hoch -9
p10 hoch -12
f10 hoch -15
a10 bis zu -18
z10 hoch -21
y10 hoch -24

Auch Konstanten, die wissenschaftlichen Daten entsprechen, werden definiert. Die aus diesen Konstanten bestehende Tabelle finden Sie hier .


Bedingte Anweisungen


Emu-bedingte Anweisungen ähneln den entsprechenden Anweisungen in Rust. Der folgende Code verwendet bedingte Konstrukte:


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

Für Schleifen


Der Header der For-Schleife ist wie for NUM in START..END , wobei NUM eine Variable ist, die Werte aus dem Bereich [START; END) [START; END) durch Einheit.


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

While-Schleifen


Der Titel der While-Schleife ist definiert als while (CONDITION) , wobei CONDITION die Bedingung ist, damit die Schleife zur nächsten Iteration CONDITION . Dieser Code ähnelt dem vorherigen Beispiel:


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

Endlose Schleifen


Endlosschleifen haben keine explizite Beendigungsbedingung und werden durch das Schlüsselwort loop definiert. Sie können jedoch durch die Anweisungen break und continue fortgesetzt oder unterbrochen werden (wie die beiden anderen Schleifentypen).


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

Rückkehr von der Funktion


Wie in allen anderen Programmiersprachen ist die return die Ausgabe der aktuellen Funktion. Es kann auch einen bestimmten Wert zurückgeben, wenn die Funktionssignatur (siehe folgende Abschnitte) dies zulässt.


 let result: i32 = 23446; return result; 

Andere Betreiber


  • Verfügbare Zuweisungsoperatoren: = , += , -= , *= , /= , %= , &= , ^= , <<= , >>= ;
  • Der Indexoperator ist [IDX] ;
  • (ARGS) - (ARGS) ;
  • Unäre Operatoren: * zum Dereferenzieren! Boolesche Daten invertieren, - Zahlen negieren;
  • Binäroperatoren: + , - , * , / , % , && , || , & , | , ^ , >> , << , > , < , >= , <= , == != .

Funktionen


Emu besteht aus drei Teilen von Funktionen: dem Bezeichner, den Parametern und dem Hauptteil der Funktion, die aus einer Folge ausführbarer Anweisungen bestehen. Betrachten Sie die Funktion des Hinzufügens von zwei Zahlen:


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

Wie Sie vielleicht bemerkt haben, gibt diese Funktion die Summe von zwei Argumenten zurück, die mit dem Datentyp f32 an sie f32 .


Adressräume


Jeder Parameter der Funktion entspricht einem bestimmten Adressraum . Standardmäßig entsprechen alle Parameter dem Bereich __private__ .


Das Hinzufügen der Präfixe global_ und local_ zur Parameterkennung gibt explizit den Adressraum an.


In der Dokumentation wird global_ für alle Vektoren das Präfix global_ zu verwenden und nichts anderes vorangestellt zu haben.


Eingebaute Funktionen


Emu bietet eine kleine Reihe integrierter Funktionen (aus OpenCL), mit denen Sie GPU-Daten verwalten können:


  • get_work_dim() - Gibt die Anzahl der Dimensionen zurück.
  • get_global_size() - Gibt die Anzahl der globalen Elemente für eine bestimmte Dimension zurück.
  • get_global_id() - Gibt den eindeutigen Bezeichner des Elements für die angegebene Dimension zurück.
  • get_global_size() - Gibt die Anzahl der globalen Elemente für eine bestimmte Dimension zurück.
  • get_local_id() - Gibt einen eindeutigen Bezeichner für ein lokales Element innerhalb einer bestimmten Arbeitsgruppe für eine bestimmte Dimension zurück.
  • get_num_groups() - Gibt die Anzahl der Arbeitsgruppen für eine bestimmte Dimension zurück.
  • get_group_id() - Gibt eine eindeutige Kennung für die Arbeitsgruppe zurück.

Im Anwendungscode finden Sie meistens den Ausdruck get_global_id(0) , der den aktuellen Index des get_global_id(0) zurückgibt, das dem Aufruf Ihrer Kernelfunktion zugeordnet ist.


Codeausführung


Berücksichtigen Sie die Syntax zum Aufrufen von Emu-Funktionen aus regulärem Rust-Code. Als Beispiel verwenden wir eine Funktion, die alle Elemente eines Vektors mit einer bestimmten Zahl multipliziert:


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

Um diese Funktion in OpenCL-Code zu übersetzen, müssen Sie ihre Signatur in das build! Makro einfügen build! wie folgt:


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

Weitere Aktionen sind das Aufrufen von Emu-Funktionen, die Sie aus Rust-Code geschrieben haben. Einfacher geht es nicht:


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

Anwendungsbeispiel


Dieses Programm verwendet als erstes Argument einen Skalar, mit dem die folgenden Argumente multipliziert werden müssen. Der resultierende Vektor wird auf der Konsole gedruckt:


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

Sie können diesen Code mit dem Befehl cargo run -- 3 2.1 3.6 6.2 ausführen. Die daraus resultierende Schlussfolgerung entspricht den Erwartungen:


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

Link zu OpenCL


Wie bereits erwähnt, ist Emu nur eine Abstraktion über OpenCL und kann daher mit der Ocl- Kiste interagieren. Der folgende Code stammt aus einem Beispiel im offiziellen Repository :


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

Fertigstellung


Ich hoffe dir hat der Artikel gefallen. Eine schnelle Antwort auf Ihre Fragen erhalten Sie im russischsprachigen Chat in Rust ( Version für Anfänger ).


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


All Articles