Hola a todos!
En esta guía quiero decir cómo hacer amigos CUDA C / C ++ y Rust. Y como ejemplo, escribiremos un pequeño programa en Rust para calcular el producto escalar de vectores, el cálculo del producto escalar se realizará en la GPU usando CUDA C.
¡A quién le importa el gato!
CUDA C
El primer paso es instalar el compilador CUDA - nvcc. ¿Qué es CUDA y por qué necesito describirlo? Puedo leer sobre esto
aquí, por ejemplo. Solo puedo decir que con su ayuda puede escribir código que se ejecutará en tarjetas de video NVIDIA (en lo sucesivo, la GPU) y utilizar toda su potencia para la computación paralela y el procesamiento de gráficos. Una vez más, este tutorial no trata sobre cómo escribir código en CUDA, sino sobre cómo usar sus ventajas del código Rust y escribir computación paralela en la GPU.
Instale nvcc y el kit de herramientas CUDA. Con esta complejidad, no deberían surgir instrucciones detalladas:
fuera del sitio .
RUST + CUDA C
En el curso de este tutorial, como se mencionó anteriormente, escribiremos un programa en Rust para encontrar el producto escalar de dos vectores, el proceso de cálculo en sí ocurrirá en la GPU.
El producto escalar de dos vectores.Supongamos que tenemos dos vectores:
y
, el producto escalar de estos vectores:
Comencemos a crear nuestro programa. Además, supongo que nvcc se instaló con éxito, rustc y cargo también significan compilar código de óxido.
Primero, cree la carpeta del proyecto. En la carpeta del proyecto, cree el archivo Cargo.toml, que contiene instrucciones para el recolector de carga. El archivo se ve así:
[package] name = "rust_cuda" # version = "0.1.0" # authors = ["MoonL1ght <ixav1@icloud.com>"] # build = "build.rs" # rust links = "cudart" # cuda, [dependencies] libc = "0.2" # rust rand = "0.5.5" # rust [build-dependencies] cc = "1.0" # rust
También en la carpeta raíz del proyecto, cree el archivo build.rs que contendrá instrucciones para construir el programa Rust y compilar el código CUDA C.
Agregue la carpeta src a la raíz del proyecto en la que colocaremos los archivos de código fuente. En la carpeta src, cree cuatro archivos: main.rs: el código del programa principal, dot.cpp: enlace de C ++ (envoltorio para CUDA C), dot_gpu.h, dot_gpu.cu: el archivo que contiene el código ejecutado en la GPU.
Total tenemos una estructura de proyecto de este tipo:
rust-cuda/ src/ main.rs dot.cpp dot_gpu.h dot_gpu.cu Cargo.toml build.rs
En el archivo build.rs, lo más importante es escribir esto:
println!("cargo:rustc-link-search=native=/Developer/NVIDIA/CUDA-10.1/lib"); println!("cargo:rustc-link-search=/Developer/NVIDIA/CUDA-10.1/lib"); println!("cargo:rustc-env=LD_LIBRARY_PATH=/Developer/NVIDIA/CUDA-10.1/lib"); println!("cargo:rustc-link-lib=dylib=cudart");
donde /Developer/NVIDIA/CUDA-10.1/lib es la ruta a los archivos ejecutables de CUDA, en un sistema tipo Unix, esta ruta se puede encontrar, por ejemplo, con el comando:
which nvcc
Además, en el archivo build.rs, debe especificar la ruta a los archivos dot.cpp y dot_gpu.cpp:
.files(&["./src/dot.cpp", "./src/dot_gpu.cu"])
Todo el código build.rs extern crate cc; fn main() { cc::Build::new() .cuda(true) .cpp(true) .flag("-cudart=shared") .files(&["./src/dot.cpp", "./src/dot_gpu.cu"]) .compile("dot.a"); println!("cargo:rustc-link-search=native=/Developer/NVIDIA/CUDA-10.1/lib"); println!("cargo:rustc-link-search=/Developer/NVIDIA/CUDA-10.1/lib"); println!("cargo:rustc-env=LD_LIBRARY_PATH=/Developer/NVIDIA/CUDA-10.1/lib"); println!("cargo:rustc-link-lib=dylib=cudart"); }
Ahora puede comenzar a escribir el código del programa principal. En el archivo main.rs, debe crear una interfaz para las funciones C / C ++ para llamar directamente desde el código Rust. Puede leer más sobre esto en la documentación oficial en la sección
FFI .
extern "C" {
Para llamarlo, debe usar el bloque de código inseguro, como argumentos pasamos un puntero mutable al tipo Vec:
unsafe { gpu_res = dot(v1.as_mut_ptr(), v2.as_mut_ptr(), VEC_SIZE); }
Código de archivo main.rs completo extern crate libc; extern crate rand; use libc::{c_float, size_t}; use rand::Rng; const VEC_SIZE: usize = 10; const MAX: f32 = 10.; const MIN: f32 = 0.; extern "C" { fn dot(v1: *mut c_float, v2: *mut c_float, N: size_t) -> c_float; } fn cpu_dot(v1: Vec<f32>, v2: Vec<f32>) -> f32 { let mut res: f32 = 0.; for i in 0..v1.len() { res += v1[i] * v2[i]; } return res; } fn main() { let mut v1: Vec<f32> = Vec::new(); let mut v2: Vec<f32> = Vec::new(); let mut gpu_res: c_float; let mut cpu_res: f32 = 0.; let mut rng = rand::thread_rng(); for _ in 0..VEC_SIZE { v1.push(rng.gen_range(MIN, MAX)); v2.push(rng.gen_range(MIN, MAX)); } println!("{:?}", v1); println!("{:?}", v2); println!("GPU computing started"); unsafe { gpu_res = dot(v1.as_mut_ptr(), v2.as_mut_ptr(), VEC_SIZE); } println!("GPU computing finished"); println!("GPU dot product result: {}", gpu_res); cpu_res = cpu_dot(v1, v2); println!("CPU dot product result: {}", cpu_res); }
Ahora comenzamos a escribir los enlaces en C ++, así como el código para calcular el producto escalar de vectores en CUDA C.
En el archivo dot.cpp, escribimos la función de enlace, en realidad llamamos a esta función desde el código Rust:
extern "C" { float dot(float *v1, float *v2, size_t N) { float *gpu_res; float res = 0.0; gpu_res = gpu_dot(v1, v2, N);
Código de archivo dot.cpp completo #include <iostream> #include "dot_gpu.h" using namespace std; void display_vector(float *v, size_t N) { cout << "["; for (size_t i = 0; i < N; i++) { cout << v[i]; if (i != N - 1) { cout << ", "; } } cout << "]" << endl; } extern "C" { float dot(float *v1, float *v2, size_t N) { cout << "Calling gpu dot product" << endl; cout << "Got two vectors from rust:" << endl; display_vector(v1, N); display_vector(v2, N); float *gpu_res; float res = 0.0; gpu_res = gpu_dot(v1, v2, N); for (int i = 0; i < blocksPerGrid; i++) { res += gpu_res[i]; } free(gpu_res); return res; } }
El siguiente es el código del archivo dot_gpu.cu en el que se realiza el cálculo principal, no explicaré el código en este tutorial, ya que no está dedicado a la programación CUDA.
dot_gpu.cu #include "dot_gpu.h" __global__ void dot__(float *v1, float *v2, float *res, int N) { __shared__ float cache [threadsPerBlock]; int tid = threadIdx.x + blockIdx.x * blockDim.x; int cacheIndex = threadIdx.x; float temp = 0.0; while (tid < N) { temp += v1[tid] * v2[tid]; tid += blockDim.x * gridDim.x; } cache[cacheIndex] = temp; __syncthreads(); int i = blockDim.x / 2; while (i != 0) { if (cacheIndex < i) { cache[cacheIndex] += cache[cacheIndex + i]; } __syncthreads(); i /= 2; } if (cacheIndex == 0) { res[blockIdx.x] = cache[0]; } } float * gpu_dot (float *v1, float *v2, size_t N) { float *dev_v1, *dev_v2, *dev_res, *res; res = new float[blocksPerGrid]; cudaMalloc((void**)&dev_v1, N * sizeof(float)); cudaMalloc((void**)&dev_v2, N * sizeof(float)); cudaMalloc((void**)&dev_res, blocksPerGrid * sizeof(float)); cudaMemcpy(dev_v1, v1, N * sizeof(float), cudaMemcpyHostToDevice); cudaMemcpy(dev_v2, v2, N * sizeof(float), cudaMemcpyHostToDevice); dot__<<<blocksPerGrid, threadsPerBlock>>>(dev_v1, dev_v2, dev_res, (int)N); cudaMemcpy(res, dev_res, blocksPerGrid * sizeof(float), cudaMemcpyDeviceToHost); cudaFree(dev_v1); cudaFree(dev_v2); cudaFree(dev_res); return res; }
Todo nuestro pequeño programa está escrito y listo para construir. Para ensamblarlo en la consola, llame al comando:
cargo build
Para correr:
cargo run
Después de compilar el programa, la carpeta de destino aparecerá en el directorio principal del proyecto. El archivo ejecutable de nuestro programa se ubicará en la carpeta: ./target/debug/
Además, si solo ejecutamos nuestro archivo ejecutable, obtenemos un error: la biblioteca dyld no está cargada. Es decir, no puede encontrar el camino a la biblioteca dinámica de Cuda. Para resolver este problema, puede registrar la variable de entorno LD_LIBRARY_PATH = path_to_CUDA_lib_directory / o ejecutar enlaces simbólicos en la carpeta Rust Toolchain para CUDA antes de iniciar el archivo ejecutable en la consola:
ln -s /Developer/NVIDIA/CUDA-10.1/lib/* /Users/Alexander/.rustup/toolchains/nightly-x86_64-apple-darwin/lib
donde /Users/Alexander/.rustup/toolchains/nightly-x86_64-apple-darwin/lib es mi camino hacia la cadena de herramientas de óxido instalada, puede diferir ligeramente para usted.
Al iniciar el programa a través de la ejecución de carga, no se produjo dicho error, porque registramos la variable de entorno LD_LIBRARY_PATH en el archivo build.rs.
Al final
Tenemos la capacidad de ejecutar el código CUDA C directamente desde el código Rust. Para verificar esto, creamos un pequeño programa, funciona con vectores y realiza todos los cálculos en la GPU. El código completo también se puede ver en
github .