12. Unsafe Rust et FFI (Foreign Function Interface)
unsafepermet de désactiver certaines garanties du compilateur pour des opérations que Rust ne peut pas vérifier. Le FFI permet d’appeler du code C depuis Rust. Ces deux outils sont nécessaires pour interagir avec CUDA (cudarc), les bibliothèques C (cuBLAS, NCCL), et pour des optimisations bas niveau.
12.1 Unsafe — Quand et Pourquoi
Opérations unsafe
5 super-pouvoirs de unsafe :
// 1. Déréférencer un pointeur brut
let ptr = &42 as *const i32;
unsafe {
println!("{}", *ptr);
}
// 2. Appeler une fonction unsafe (FFI)
unsafe {
libc::printf(b"hello\0".as_ptr() as *const i8);
}
// 3. Accéder à un union (C-like)
// 4. Modifier une variable statique mutable
static mut COUNTER: usize = 0;
unsafe {
COUNTER += 1;
}
// 5. Implémenter un trait unsafe (Send, Sync)
struct PtrWrapper(*mut f64);
unsafe impl Send for PtrWrapper {}
unsafe impl Sync for PtrWrapper {}Règles de l’unsafe
// UNSAFE ne désactive PAS le borrow checker !
// Il donne accès à 5 opérations supplémentaires.
// ❌ Ceci est toujours interdit (même dans unsafe) :
let mut v = vec![1, 2, 3];
let r1 = &v;
unsafe {
let r2 = &mut v; // ⛔ violation de borrowing !
}Exemple : Allouer un buffer contigu sur le heap
use std::alloc::{alloc, Layout, dealloc};
struct AlignedBuffer {
ptr: *mut u8,
size: usize,
}
impl AlignedBuffer {
fn new(size: usize, alignment: usize) -> Self {
let layout = Layout::from_size_align(size, alignment).unwrap();
let ptr = unsafe { alloc(layout) };
if ptr.is_null() { panic!("allocation échouée"); }
Self { ptr, size }
}
fn as_slice(&self) -> &[u8] {
unsafe { std::slice::from_raw_parts(self.ptr, self.size) }
}
fn as_mut_slice(&mut self) -> &mut [u8] {
unsafe { std::slice::from_raw_parts_mut(self.ptr, self.size) }
}
}
impl Drop for AlignedBuffer {
fn drop(&mut self) {
let layout = Layout::from_size_align(self.size, 64).unwrap();
unsafe { dealloc(self.ptr, layout); }
}
}
// Utilisation : buffer aligné pour transfert GPU
let mut buf = AlignedBuffer::new(1024 * 1024, 256); // 1 Mo, aligné à 256 octets
buf.as_mut_slice()[0] = 42;12.2 FFI — Appeler du C depuis Rust
Déclarer des fonctions C
extern "C" {
fn cudaMalloc(ptr: *mut *mut std::ffi::c_void, size: usize) -> i32;
fn cudaMemcpy(dst: *mut std::ffi::c_void, src: *const std::ffi::c_void,
size: usize, kind: i32) -> i32;
fn cudaFree(ptr: *mut std::ffi::c_void) -> i32;
}
// Constantes
const cudaMemcpyDeviceToHost: i32 = 2;
const cudaMemcpyHostToDevice: i32 = 1;
// Wrapper sûr
fn allocate_gpu(size: usize) -> Result<*mut std::ffi::c_void, String> {
let mut ptr: *mut std::ffi::c_void = std::ptr::null_mut();
let err = unsafe { cudaMalloc(&mut ptr as *mut *mut _, size) };
if err != 0 {
Err(format!("cudaMalloc a échoué avec code {err}"))
} else {
Ok(ptr)
}
}Structures compatibles C
#[repr(C)] // garantit l'ordre et l'alignement C (pas de réorganisation)
struct CudaBuffer {
ptr: *mut f64, // 8 octets
size: usize, // 8 octets (sur 64-bit)
}
#[repr(C, packed)] // pas de padding (attention : alignement potentiellement lent)
struct PackedGradient {
value: f64,
worker_id: i32,
}Intégration avec un build script
// build.rs — exécuté avant la compilation
fn main() {
println!("cargo:rustc-link-lib=cudart"); // lie CUDA runtime
println!("cargo:rustc-link-search=/usr/local/cuda/lib64");
}
// Puis dans src/
extern "C" {
fn cudaSetDevice(device: i32) -> i32;
}12.3 Intégration CUDA avec cudarc
// Exemple cudarc (bindings CUDA Driver API, pas de kernel writing)
use cudarc::driver::*;
use std::rc::Rc;
fn median_on_gpu(grads: &[Vec<f64>]) -> Result<Vec<f64>, Box<dyn std::error::Error>> {
let dev = CudaDevice::new(0)?; // premier GPU
let n = grads.len();
let d = grads[0].len();
let total_size = n * d;
// Allouer sur GPU
let d_grads = dev.alloc::<f64>(total_size)?; // DeviceBuffer<f64>
let d_result = dev.alloc::<f64>(d)?;
// Copier CPU → GPU
let flat: Vec<f64> = grads.iter().flat_map(|g| g.iter().copied()).collect();
d_grads.copy_from(&flat)?;
// Lancer un kernel CUDA (écrit en .cu, chargé ici)
// La fonction "median_kernel" est définie dans un fichier .cu compilé
let ptx = std::fs::read_to_string("kernels/median.ptx")?;
dev.load_ptx("median.ptx", "median_module", &[&ptx])?;
let kernel = dev.get_kernel("median_module", "median_kernel")?;
unsafe {
dev.launch_kernel1d(
d as u32, // 1 bloc par dimension
kernel,
(&d_grads, n as i32, d as i32, &d_result),
)?;
}
// Copier GPU → CPU
let mut result = vec![0.0; d];
d_result.copy_into(&mut result)?;
Ok(result)
}Avec des kernels CUDA C++ compilés à côté
// kernels/median.cu
extern "C" __global__ void median_kernel(
const double* grads, int n, int d, double* result
) {
int j = blockIdx.x; // une dimension par bloc
if (j >= d) return;
// Copier la colonne j dans la mémoire partagée
extern __shared__ double col[];
for (int i = 0; i < n; i++) {
col[i] = grads[i * d + j];
}
// Trier (bitonic sort pour la démo)
for (int i = 0; i < n - 1; i++) {
for (int k = 0; k < n - i - 1; k++) {
if (col[k] > col[k + 1]) {
double tmp = col[k];
col[k] = col[k + 1];
col[k + 1] = tmp;
}
}
}
result[j] = col[n / 2];
}12.4 Exposer Rust en C (l’inverse)
// src/lib.rs — librairie Rust appelable depuis C/Python
#[no_mangle] // empêche le name mangling Rust
pub extern "C" fn median_aggregate(
grads: *const f64,
n: i32,
d: i32,
result: *mut f64,
) -> i32 {
// Convertir les pointeurs C en slices Rust
let n = n as usize;
let d = d as usize;
let grads_slice = unsafe {
std::slice::from_raw_parts(grads, n * d)
};
let result_slice = unsafe {
std::slice::from_raw_parts_mut(result, d)
};
// Construire la matrice
let mut matrix: Vec<Vec<f64>> = Vec::with_capacity(n);
for i in 0..n {
let start = i * d;
matrix.push(grads_slice[start..start + d].to_vec());
}
// Calculer la médiane
for j in 0..d {
let mut col: Vec<f64> = matrix.iter().map(|g| g[j]).collect();
col.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap());
result_slice[j] = col[n / 2];
}
0 // succès
}# Compiler en librairie C dynamique
cargo build --release
# target/release/libgradient_core.so (Linux)
# target/release/libgradient_core.dylib (macOS)
# target/release/gradient_core.dll (Windows)# Python — appeler la librairie via ctypes sans PyO3
import ctypes
lib = ctypes.CDLL("target/release/libgradient_core.dylib")
lib.median_aggregate.argtypes = [
ctypes.POINTER(ctypes.c_double),
ctypes.c_int32,
ctypes.c_int32,
ctypes.POINTER(ctypes.c_double),
]
lib.median_aggregate.restype = ctypes.c_int32
grads = [1.0, 5.0, 3.0, 2.0, 2.0, 8.0, 9.0, 1.0, 4.0]
n, d = 3, 3
grads_arr = (ctypes.c_double * len(grads))(*grads)
result_arr = (ctypes.c_double * d)()
lib.median_aggregate(grads_arr, n, d, result_arr)
print(list(result_arr)) # [3.0, 2.0, 4.0]12.5 Règles pour l’Unsafe
| Règle | Explication |
|---|---|
| Minimiser l’unsafe | L’encapsuler dans la plus petite fonction possible |
| Wrapper sûr | Toujours proposer une API safe autour de l’unsafe |
| Documenter les invariants | // SAFETY: ptr doit être non-null et aligné |
| Tester | Les fonctions unsafe nécessitent encore plus de tests |
| Miri | Utiliser cargo miri test pour détecter les UB |
| Préférer les crates | Ne pas réinventer : cudarc, libc, winapi |
/// Calcule la somme d'un tableau de f64.
///
/// # Safety
/// - `ptr` doit pointer sur un tableau valide de `len` éléments
/// - `len` doit être > 0
unsafe fn sum_array(ptr: *const f64, len: usize) -> f64 {
let slice = unsafe { std::slice::from_raw_parts(ptr, len) };
slice.iter().sum()
}