| name | rust-candle-core |
| description | Build native Rust ML models with Candle framework. Use when implementing vision transformers, LLMs, or audio models with GPU acceleration. |
Candle ML Framework
Pure Rust ML framework for building and running neural networks with GPU acceleration.
Setup
[dependencies]
candle-core = "0.4"
candle-nn = "0.4"
candle-transformers = "0.4"
hf-hub = "0.3"
tokenizers = "0.15"
image = "0.25"
symphonia = { version = "0.5", features = ["all"] }
[features]
cuda = ["candle-core/cuda", "candle-nn/cuda", "candle-transformers/cuda"]
Device Initialization
use candle_core::Device;
fn init_device() -> Result<Device> {
#[cfg(feature = "cuda")]
{
if let Ok(device) = Device::new_cuda(0) {
tracing::info!("Using CUDA device");
return Ok(device);
}
}
tracing::info!("Using CPU device");
Ok(Device::Cpu)
}
Loading Models from HuggingFace Hub
use candle_core::{DType, Device};
use candle_nn::VarBuilder;
use hf_hub::{Repo, RepoType};
use std::path::{Path, PathBuf};
fn load_model(model_id: &str, cache_dir: Option<&Path>, device: &Device) -> Result<VarBuilder> {
let cache_path = cache_dir
.map(|p| p.to_path_buf())
.unwrap_or_else(|| PathBuf::from("models/hf"));
std::env::set_var("HF_HOME", &cache_path);
let api = hf_hub::api::sync::ApiBuilder::new()
.with_cache_dir(cache_path)
.build()?;
let repo = api.repo(Repo::new(model_id.to_string(), RepoType::Model));
let weights_path = repo.get("model.safetensors")?;
let vb = unsafe {
VarBuilder::from_mmaped_safetensors(&[weights_path], DType::F32, device)?
};
Ok(vb)
}
Core Tensor Operations
use candle_core::{DType, Device, Tensor, D};
use candle_nn::ops::softmax;
let data = vec![1.0f32, 2.0, 3.0, 4.0];
let tensor = Tensor::from_vec(data, (2, 2), &device)?;
let reshaped = tensor.reshape((1, 4))?;
let transposed = tensor.transpose(0, 1)?;
let result = a.matmul(&b)?;
let probs = softmax(&logits, D::Minus1)?;
fn l2_normalize(embeddings: &Tensor) -> Result<Tensor> {
let norm = embeddings
.sqr()?
.sum_keepdim(D::Minus1)?
.sqrt()?
.clamp(1e-12, f64::MAX)?;
embeddings.broadcast_div(&norm)
}
= probs
.(D::Minus1)?
.(DType::U32)?
.to_vec1::<>()?[];
Vision Transformer Patterns
Patch Embedding
use candle_nn::{conv2d, Conv2d, Conv2dConfig, Module, VarBuilder};
struct PatchEmbed {
proj: Conv2d,
num_patches: usize,
}
impl PatchEmbed {
fn new(vb: VarBuilder, in_channels: usize, embed_dim: usize, patch_size: usize) -> Result<Self> {
let proj = conv2d(
in_channels,
embed_dim,
patch_size,
Conv2dConfig {
stride: patch_size,
..Default::default()
},
vb.pp("proj"),
)?;
Ok(Self { proj, num_patches: (224 / patch_size).pow(2) })
}
}
impl Module for PatchEmbed {
fn forward(&self, xs: &Tensor) -> candle_core::Result<Tensor> {
let xs = self.proj.forward(xs)?;
let (b, c, _h, _w) = xs.dims4()?;
xs.reshape((b, c, self.num_patches))?.transpose(1, )
}
}
Multi-Head Attention
CRITICAL: Always call .contiguous() before matmul() - transposes and scalar ops create non-contiguous views!
use candle_nn::{linear, Linear, VarBuilder};
struct Attention {
qkv: Linear,
proj: Linear,
num_heads: usize,
head_dim: usize,
scale: f64,
}
impl Attention {
fn new(vb: VarBuilder, dim: usize, num_heads: usize) -> Result<Self> {
let head_dim = dim / num_heads;
Ok(Self {
qkv: linear(dim, dim * 3, vb.pp("qkv"))?,
proj: linear(dim, dim, vb.pp("proj"))?,
num_heads,
head_dim,
scale: 1.0 / (head_dim as f64).sqrt(),
})
}
fn forward(&self, xs: &Tensor) -> Result<Tensor> {
let (b, n, c) = xs.dims3()?;
let qkv = self.qkv.forward(xs)?;
let qkv = qkv.reshape((b, n, 3, self.num_heads, self.head_dim))?;
= qkv.((, , , , ))?;
= qkv.()?;
= qkv.()?;
= qkv.()?;
= (q * .scale)?.()?;
= q.(&k.(D::Minus2, D::Minus1)?.()?)?;
= (&attn, D::Minus1)?;
= attn.()?.(&v.()?)?;
= out.(, )?.()?.((b, n, c))?;
.proj.forward(&out)
}
}
Image Preprocessing
const IMAGE_MEAN: [f32; 3] = [0.485, 0.456, 0.406];
const IMAGE_STD: [f32; 3] = [0.229, 0.224, 0.225];
fn load_image(path: &Path, device: &Device) -> Result<Tensor> {
let img = image::open(path)?;
let img = img.resize_exact(224, 224, image::imageops::FilterType::Triangle);
let img = img.to_rgb8();
let (width, height) = (img.width() as usize, img.height() as usize);
let data = img.into_raw();
let mut normalized = vec![0.0f32; 3 * height * width];
for c in 0..3 {
for h in ..height {
..width {
= (h * width + w) * + c;
= c * height * width + h * width + w;
= data[src_idx] / ;
normalized[dst_idx] = (pixel - IMAGE_MEAN[c]) / IMAGE_STD[c];
}
}
}
Tensor::(normalized, (, , height, width), device)
}
LLM Patterns
Rotary Position Embeddings (RoPE)
struct RotaryEmbedding {
cos: Tensor,
sin: Tensor,
}
impl RotaryEmbedding {
fn new(dim: usize, max_seq_len: usize, device: &Device) -> Result<Self> {
let theta = 10000.0f32;
let half_dim = dim / 2;
let freqs: Vec<f32> = (0..half_dim)
.map(|i| 1.0 / theta.powf(2.0 * i as f32 / dim as f32))
.collect();
let positions: Vec<f32> = (0..max_seq_len).map(|i| i as f32).collect();
let mut cos_cache = vec![0.0f32; max_seq_len * half_dim];
let mut sin_cache = vec![0.0f32; max_seq_len * half_dim];
for (pos_idx, &pos) in positions.().() {
(freq_idx, &freq) freqs.().() {
= pos * freq;
cos_cache[pos_idx * half_dim + freq_idx] = angle.();
sin_cache[pos_idx * half_dim + freq_idx] = angle.();
}
}
( {
cos: Tensor::(cos_cache, (max_seq_len, half_dim), device)?,
sin: Tensor::(sin_cache, (max_seq_len, half_dim), device)?,
})
}
(&, x: &Tensor, start_pos: ) <Tensor> {
(_, _, seq_len, dim) = x.()?;
= dim / ;
= x.(D::Minus1, , half)?;
= x.(D::Minus1, half, half)?;
= .cos.(, start_pos, seq_len)?.()?.()?;
= .sin.(, start_pos, seq_len)?.()?.()?;
= x1.(&cos)?.(&x2.(&sin)?)?;
= x1.(&sin)?.(&x2.(&cos)?)?;
Tensor::(&[rotated_x1, rotated_x2], D::Minus1)
}
}
Causal Attention Mask
fn create_causal_mask(seq_len: usize, device: &Device) -> Result<Tensor> {
let mut mask_data = vec![0.0f32; seq_len * seq_len];
for i in 0..seq_len {
for j in 0..seq_len {
if j > i {
mask_data[i * seq_len + j] = f32::NEG_INFINITY;
}
}
}
Tensor::from_vec(mask_data, (1, 1, seq_len, seq_len), device)
}
RMS Normalization
struct RmsNorm {
weight: Tensor,
eps: f64,
}
impl RmsNorm {
fn new(vb: VarBuilder, dim: usize, eps: f64) -> Result<Self> {
let weight = vb.get(dim, "weight")?;
Ok(Self { weight, eps })
}
fn forward(&self, xs: &Tensor) -> Result<Tensor> {
let variance = xs.sqr()?.mean_keepdim(D::Minus1)?;
let xs = xs.broadcast_div(&(variance + self.eps)?.sqrt()?)?;
xs.broadcast_mul(&self.weight)
}
}
Audio Model Patterns
Mel Spectrogram
const N_FFT: usize = 400;
const HOP_LENGTH: usize = 160;
const N_MELS: usize = 128;
fn pcm_to_mel(samples: &[f32], filters: &[f32], device: &Device) -> Result<Tensor> {
let n_frames = (samples.len() - N_FFT) / HOP_LENGTH + 1;
let hann_window: Vec<f32> = (0..N_FFT)
.map(|i| 0.5 * (1.0 - (2.0 * std::f32::consts::PI * i as f32 / N_FFT as f32).cos()))
.collect();
let fft_size = N_FFT / 2 + 1;
let mut magnitudes = vec![0.0f32; n_frames * fft_size];
for frame_idx in 0..n_frames {
let start = frame_idx * HOP_LENGTH;
let : <> = samples[start..start + N_FFT]
.()
.(&hann_window)
.(|(s, w)| s * w)
.();
..fft_size {
= ;
= ;
(n, &sample) windowed.().() {
= - * std::::consts::PI * k * n / N_FFT ;
real += sample * angle.();
imag += sample * angle.();
}
magnitudes[frame_idx * fft_size + k] = real * real + imag * imag;
}
}
= [; n_frames * N_MELS];
..n_frames {
..N_MELS {
= ;
..fft_size {
sum += filters[mel * fft_size + k] * magnitudes[frame * fft_size + k];
}
mel_spec[frame * N_MELS + mel] = sum.().();
}
}
Tensor::(mel_spec, (, N_MELS, n_frames), device)
}
Audio Loading with Symphonia
use symphonia::core::audio::SampleBuffer;
use symphonia::core::codecs::DecoderOptions;
use symphonia::core::formats::FormatOptions;
use symphonia::core::io::MediaSourceStream;
fn load_audio(path: &Path) -> Result<Vec<f32>> {
let file = std::fs::File::open(path)?;
let mss = MediaSourceStream::new(Box::new(file), Default::default());
let probed = symphonia::default::get_probe()
.format(&Default::default(), mss, &FormatOptions::default(), &Default::default())?;
let mut format = probed.format;
let track = format.default_track().ok_or("No audio track")?;
let mut decoder = symphonia::default::get_codecs()
.make(&track.codec_params, &DecoderOptions::default())?;
let track_id = track.id;
let mut = ::();
{
= format.() {
(p) => p,
(_) => ,
};
packet.() != track_id { ; }
= decoder.(&packet)?;
= *decoded.();
= SampleBuffer::<>::(decoded.() , spec);
sample_buf.(decoded);
= sample_buf.();
spec.channels.() > {
= spec.channels.();
channel_samples.(channels) {
samples.(chunk.().sum::<>() / channels );
}
} {
samples.(channel_samples);
}
}
(samples)
}
VRAM Estimation
fn estimate_vram_gb(
hidden_size: usize,
num_layers: usize,
vocab_size: usize,
intermediate_size: usize,
) -> f32 {
let embedding_params = vocab_size * hidden_size;
let attention_params = num_layers * 4 * hidden_size * hidden_size;
let mlp_params = num_layers * 3 * hidden_size * intermediate_size;
let norm_params = num_layers * hidden_size * 2;
let total = embedding_params + attention_params + mlp_params + norm_params;
(total as f32 * 4.0 * 1.2) / (1024.0 * 1024.0 * 1024.0)
}
Global Model Caching
use std::sync::OnceLock;
use parking_lot::Mutex;
static MODEL: OnceLock<Mutex<MyModel>> = OnceLock::new();
pub fn get_model() -> &'static Mutex<MyModel> {
MODEL.get_or_init(|| {
tracing::info!("Loading model (first use)...");
Mutex::new(MyModel::load_default().expect("Failed to load model"))
})
}
pub fn preload_model() -> Result<()> {
let _ = get_model();
Ok(())
}
Guidelines
- Use
cuda feature for GPU acceleration
- Memory-map weights with
from_mmaped_safetensors for efficient loading
- Cache models globally with
OnceLock to avoid reloading
- Estimate VRAM before loading models to prevent OOM
- Use pre-norm transformer blocks (norm before attention/MLP)
- L2 normalize embeddings for similarity search
- Use tracing for observability in model loading/inference
Examples
- Vision:
hercules-local-algo/src/dinov3/ - DINOv3 ViT implementation
- LLM:
hercules-local-algo/src/qwen3/ - Qwen3 decoder model
- Audio:
hercules-local-algo/src/clap/ - CLAP audio encoder
- Speech:
hercules-local-algo/src/whisper/ - Whisper transcription