1. Pourquoi Candle
Sections
Réglages de lecture
Aucune voix pour cette langue dans ce navigateur.
Plus de voix
Code et termes techniques en anglais
Désactivé : la voix de la page lit tout, sans pause entre deux voix.
Aucune voix pour cette langue dans ce navigateur.
Plus de voix
D’autres voix : Microsoft Edge propose des voix naturelles en ligne, et Windows (Paramètres › Heure et langue › Voix), macOS et Android en installent d’autres.
Candle est un framework d’apprentissage automatique écrit en Rust par Hugging Face. Il fournit des tenseurs, la différentiation automatique, des couches de réseaux de neurones et des implémentations de modèles connus, et il se compile dans ton programme comme n’importe quelle autre crate : pas de Python, pas de runtime C++ à installer à côté. Cette leçon le situe parmi les outils qu’un développeur C# ou Java connaît peut-être déjà, démonte le workspace, et exécute un premier programme.
Où se place Candle
Section intitulée « Où se place Candle »| Langage que tu écris | Ce qui calcule | Entraînement | Ce que tu livres | |
|---|---|---|---|---|
| PyTorch | Python | libtorch, en C++ et CUDA | oui | un environnement Python, ou un modèle exporté pour un autre runtime |
| tch-rs | Rust | libtorch, via une API C | oui | ton binaire plus les bibliothèques libtorch |
| TorchSharp | C#, F# | libtorch, via des paquets natifs | oui | ton application .NET plus un paquet NuGet libtorch-* |
| ONNX Runtime | C#, Java, Python, C++, Rust via ort |
son propre moteur C++ | l’inférence d’abord | ton application plus le runtime natif et un fichier .onnx |
| ML.NET | C#, F# | des entraîneurs .NET, et ONNX Runtime ou TorchSharp pour l’apprentissage profond | oui | une application .NET |
| DJL | Java | un moteur que tu choisis : PyTorch, ONNX Runtime et d’autres | selon le moteur | une application JVM plus les bibliothèques natives du moteur |
| Burn | Rust | des backends derrière un trait Backend, de CUDA à un backend CPU en Rust pur |
oui | un binaire Rust |
| Candle | Rust | du code Rust, avec CUDA, Metal, MKL ou Accelerate en option | oui | un binaire Rust, ou un module WebAssembly |
Sources : les README de tch-rs, TorchSharp, ort, DJL, Burn et Candle.
La colonne qui compte pour ce cours est la dernière. Avec tch-rs ou TorchSharp, les calculs sur les tenseurs se font dans libtorch, une bibliothèque native de plusieurs centaines de mégaoctets que tu déploies à côté de ton programme. Avec ONNX Runtime, tu déploies sa bibliothèque native et un modèle exporté depuis Python. Le README de Candle énonce ses objectifs dans les mêmes termes : rendre possible l’inférence serverless avec de petits binaires qui démarrent vite, et retirer Python des charges de production.
Ce que tu perds, c’est l’étendue. PyTorch a des milliers d’opérations et un vaste écosystème ; Candle a les opérations dont ses modèles ont besoin. La leçon 2 rencontre certaines de ses limites, comme des opérations qui paniquent sur des tenseurs entiers au lieu de renvoyer une erreur.
Le workspace
Section intitulée « Le workspace »Candle est publié sous forme de plusieurs crates. La version étudiée ici est 0.11.0, la dernière sur crates.io (26 juin 2026), dont les sources sont le tag 0.11.0, commit 31f35b1. Chaque lien vers le code de Candle dans ce cours pointe vers ce commit.
| Crate | Ce qu’elle contient | Leçons |
|---|---|---|
candle-core |
Tensor, DType, Device, les backends CPU, CUDA et Metal, l’autodiff, les tenseurs quantifiés, les formats safetensors et GGUF |
2 à 4, 8 |
candle-nn |
couches (Linear, Embedding, normalisations), activations, pertes, optimiseurs, VarBuilder |
5, 6 |
candle-transformers |
implémentations de modèles publiés : 125 entrées sous models/, de BERT à Qwen3 quantifié |
7, 8 |
candle-datasets |
chargeurs de jeux de données courants comme MNIST | 5 |
candle-examples |
111 programmes en ligne de commande, un par modèle ou par tâche | 7 à 9 |
candle-onnx |
un évaluateur de graphes ONNX | 11 |
candle-pyo3 |
des bindings Python, la direction inverse de ce cours | — |
candle-kernels, candle-metal-kernels, candle-flash-attn |
noyaux GPU en CUDA et Metal | 9 |
candle-wasm-examples |
les mêmes modèles compilés en WebAssembly pour le navigateur | 10 |
candle-core active ses backends avec des features Cargo, déclarées dans candle-core/Cargo.toml, lignes 48-62. Aucune n’est active par défaut, donc une dépendance simple ne te donne que le backend CPU.
| Feature | Ce qu’elle ajoute | Où |
|---|---|---|
cuda |
le backend CUDA, via la crate cudarc, et les noyaux CUDA |
GPU NVIDIA, demande le toolkit CUDA |
cudnn |
cuda plus cuDNN pour les convolutions |
idem |
nccl |
cuda plus NCCL, pour répartir un modèle sur plusieurs GPU |
idem |
metal |
le backend Metal | GPU Apple, macOS |
accelerate |
le framework Accelerate d’Apple pour les produits matriciels sur le CPU | macOS |
mkl |
la Math Kernel Library d’Intel pour les produits matriciels sur le CPU | CPU x86 |
candle-nn et candle-transformers transmettent les mêmes features à candle-core. La leçon 9 couvre les features GPU ; le code de ce cours n’en utilise aucune.
Exécuter le code
Section intitulée « Exécuter le code »Le code du cours est un workspace Cargo dans code/candle, avec un exemple par sujet et un par série de solutions d’exercices. Il te faut Rust et un compilateur C, pour une raison expliquée plus bas : sous Windows, les Visual Studio Build Tools que rustup demande suffisent.
cd code\candlecargo run --release --example l01_hellobash check.shcd code/candlecargo run --release --example l01_hellobash check.shcd code/candlecargo run --release --example l01_hellobash check.shcheck.sh exécute cargo fmt --check, clippy, les tests (dont les doctests compile_fail des extraits que ce cours montre refusés), puis chaque exemple, et compare chaque sortie avec le fichier de expected/. Sous Windows, lance-le depuis Git Bash.
Le premier programme
Section intitulée « Le premier programme »Le livre de Candle ajoute candle-core avec cargo add --git, qui suit la branche main. Ce cours épingle plutôt la version publiée, pour que les sorties ci-dessous restent vraies : Cargo.toml demande exactement 0.11.0 avec =, comme le décrit la référence Cargo, et Cargo.lock est commité.
[workspace.dependencies]candle-core = "=0.11.0"candle-nn = "=0.11.0"Le programme est celui du README de Candle, un produit matriciel, avec un changement. Le README remplit les matrices avec Tensor::randn, des nombres aléatoires, et le générateur aléatoire du CPU de Candle ne peut pas recevoir de graine (la leçon 2 montre l’erreur), donc sa sortie change à chaque exécution. examples/l01_hello.rs utilise arange à la place :
use candle_core::{Device, Tensor};
fn main() -> Result<(), Box<dyn std::error::Error>> { let device = Device::Cpu;
// arange au lieu du randn du README : les mêmes nombres à chaque exécution et sur chaque OS let a = Tensor::arange(0f32, 6., &device)?.reshape((2, 3))?; let b = Tensor::arange(0f32, 12., &device)?.reshape((3, 4))?;
let c = a.matmul(&b)?; println!("{c}"); Ok(())}[[20., 23., 26., 29.], [56., 68., 80., 92.]]Tensor[[2, 4], f32]Quatre choses à remarquer, quand on vient de C# ou de Java :
- Chaque opération renvoie un
Result.arange,reshapeetmatmulpeuvent toutes échouer, sur une forme qui ne convient pas par exemple, et elles le signalent par une valeurcandle_core::Errorau lieu de lever une exception. Le?après chaque appel renvoie l’erreur à l’appelant, comme l’explique le cours Rust. Là où TorchSharp lève une exception, Candle te remet une valeur. - Le périphérique est explicite. Chaque tenseur est créé sur un
Device:Device::Cpuici, un périphérique CUDA ou Metal à la leçon 9. - La forme et le type des éléments sont des valeurs d’exécution.
Tensorest un seul type pour toutes les formes et tous les types d’éléments ; la dernière ligne de la sortie affiche les deux,[2, 4]etf32. Une erreur de forme se découvre quand le programme s’exécute, pas quand il compile. 0f32, 6.donne un suffixe de type au premier nombre ; Rust en déduitf32pour le second.
Une matrice (2, 3) fois une matrice (3, 4) donne une matrice (2, 4) : le premier élément vaut 0·0 + 1·4 + 2·8 = 20.
Deux erreurs que tout débutant rencontre
Section intitulée « Deux erreurs que tout débutant rencontre »Afficher le résultat de matmul sans ? affiche un Result, pas un tenseur, et Result n’implémente pas Display :
let c = a.matmul(&b);println!("{c}");error[E0277]: `Result<candle_core::Tensor, candle_core::Error>` doesn't implement `std::fmt::Display` --> course\examples\x_result.rs:8:15 |8 | println!("{c}"); | ^^^ `Result<candle_core::Tensor, candle_core::Error>` cannot be formatted with the default formatterLa seconde vient de l’aide-mémoire du propre README de Candle, ligne 278, qui convertit un tenseur avec tensor.to_dtype(&DType::F16)?. to_dtype prend le DType par valeur :
error[E0308]: mismatched types --> course\examples\x_readme.rs:5:32 | 5 | let half = tensor.to_dtype(&DType::F16)?; | -------- ^^^^^^^^^^^ expected `DType`, found `&DType` | | | arguments to this method are incorrectDType est une petite enum Copy, il n’y a donc aucune raison de passer une référence ; la suggestion du compilateur, retirer le &, est la correction. Les deux extraits sont des doctests compile_fail dans src/lib.rs, pour que le cours s’aperçoive si une version ultérieure de Candle ou de Rust les accepte.
Ce que coûte la compilation
Section intitulée « Ce que coûte la compilation »Le premier cargo build --release de ce workspace a pris 73 secondes et compilé 128 crates, sur un Intel Core Ultra 9 285K sous Windows 11 avec Rust 1.94.0, limité à 4 tâches parallèles (-j 4) parce que la machine était partagée. L’exécutable l01_hello fait 1,4 Mo. Les builds suivants ne recompilent que la crate du cours, en une seconde environ.
La plupart de ces crates sont celles qu’on attend : gemm pour les produits matriciels, rayon pour les threads, half pour les flottants 16 bits, safetensors pour les fichiers de modèle. Une ne l’est pas. cargo tree montre par où arrive le compilateur C :
> cargo tree -p candle-course -e normal -i onig_sysonig_sys v69.9.3└── onig v6.5.3 └── tokenizers v0.22.2 └── candle-core v0.11.0candle-core 0.11.0 dépend de la crate tokenizers de Hugging Face sur toutes les cibles sauf WebAssembly (Cargo.toml, lignes 37-38), et avec sa feature onig, qui compile la bibliothèque d’expressions régulières Oniguruma depuis ses sources C. Un seul fichier s’en sert, quantized/tokenizer.rs, qui reconstruit un tokenizer à partir des métadonnées d’un fichier GGUF (leçon 8). Sur les 119 crates derrière candle-core, 72 sont dans l’arbre de dépendances de tokenizers. Sur main, après 0.11.0, la dépendance est toujours là, mais avec la feature fancy-regex en Rust pur au lieu de onig (Cargo.toml à ddf1b87), donc l’exigence d’un compilateur C devrait disparaître avec la prochaine version (à vérifier quand elle sera publiée).
Demander un GPU
Section intitulée « Demander un GPU »examples/l01_devices.rs demande au build quels backends il possède, puis demande un périphérique CUDA et un périphérique Metal :
println!("cuda feature: {}", utils::cuda_is_available());match Device::new_cuda(0) { Ok(d) => println!("Device::new_cuda(0): {d:?}"), Err(e) => println!("Device::new_cuda(0): error: {e}"),}// Se rabat sur le CPU au lieu d'échouerlet device = Device::cuda_if_available(0)?;cuda feature: falsemetal feature: falsemkl feature: falseaccelerate feature: falseDevice::new_cuda(0): error: the candle crate has not been built with cuda supportDevice::new_metal(0): error: the candle crate has not been built with metal supportDevice::cuda_if_available(0): CpuLa sortie est la même sur une machine avec un GPU NVIDIA : utils::cuda_is_available indique si la feature cuda a été compilée, pas si un GPU est présent. Sans la feature, Candle compile un backend de substitution dont les constructeurs renvoient cette erreur. cuda_if_available est la façon habituelle d’écrire un programme qui utilise un GPU quand il a été compilé pour, et le CPU sinon.
Le backend CPU a un autre interrupteur qui n’est pas une feature. examples/l01_machine.rs affiche ce que le build sait du processeur ; check.sh l’exécute sans comparer, puisque cela dépend de la machine :
os / arch: windows / x86_64compiled for avx2: falsecompiled for neon: falsecompiled for f16c: falsethreads (rayon): 24Le processeur a AVX2, mais le programme n’a pas été compilé pour : Rust cible un CPU x86-64 de base sauf si tu passes -C target-cpu=native. Le dépôt de Candle le fait, dans .cargo/config.toml, donc ses exemples sont compilés pour la machine sur laquelle ils tournent, et une crate qui dépend de Candle ne l’est pas. La leçon 3 mesure ce que cela change.
Dans GA, IX et TARS
Section intitulée « Dans GA, IX et TARS »Aucun des trois dépôts que ce site étudie ne dépend de Candle aujourd’hui.
- GA exécute ses modèles d’embeddings en C# avec ONNX Runtime :
GA.Business.ML.csprojréférenceMicrosoft.ML.OnnxRuntime1.23.2 etMicrosoft.ML.Tokenizers2.0.0, etOnnxEmbeddingService.csouvre uneInferenceSession. C’est la ligne « ONNX Runtime » du tableau ci-dessus ; la leçon 7 calcule des embeddings avec Candle, et la leçon 11 appelle un modèle Candle depuis C#. - TARS fait de même depuis F# :
TarsEngine.FSharp.Cli.fsprojréférence ONNX Runtime 1.19.2 etMicrosoft.ML.Tokenizers0.22.0. - IX est écrit en Rust et a choisi de ne pas s’appuyer sur Candle. Sa crate de différentiation automatique,
ix-autograd, est écrite à partir de zéro au-dessus dendarray; sa documentation consigne cette décision (lib.rs, ligne 27) et garde une place pour « a future Candle or GPU backend » (tensor.rs, lignes 9-19). La leçon 4 fait passer le même gradient par les deux.
Le guide d’IX sur les bibliothèques Rust décrit Candle comme « optimised for LLM inference on NVIDIA GPUs via cuTENSOR and cuDNN » (code-analysis-tools.md, ligne 129). Les features CUDA du workspace de Candle demandent à cudarc cuBLAS, cuBLASLt, cuRAND et NVRTC, avec cuDNN en option (Cargo.toml, lignes 50-61) ; cuTENSOR n’en fait pas partie, et les backends CPU, Metal et WebAssembly ne sont pas mentionnés. Le journal le recense.
À retenir
Section intitulée « À retenir »- Candle est un framework Rust pour les tenseurs, l’autodiff et les modèles, compilé dans ton binaire. Son argument face à tch-rs, TorchSharp et ONNX Runtime est qu’il n’y a aucun runtime natif à livrer.
candle-corecontient les tenseurs et les backends,candle-nnles couches et les optimiseurs,candle-transformersles modèles. Les backends GPU et MKL ou Accelerate sont des features Cargo à activer.- Chaque opération sur un tenseur renvoie un
Result; les formes et les types d’éléments sont vérifiés à l’exécution. - Épingle la version de crates.io avec
=pour des résultats reproductibles. La version 0.11.0 demande un compilateur C parce quecandle-coretiretokenizersavec Oniguruma. cuda_is_availableindique une feature compilée, pas le matériel. Sans-C target-cpu=native, les noyaux propres au CPU derrièrewith_avxne sont pas compilés.
Exercices
Section intitulée « Exercices »Les solutions sont dans examples/l01_exercises.rs, et leur sortie dans expected/l01_exercises.txt.
- Dans
l01_hello, calculeb.matmul(&a)au lieu dea.matmul(&b). Lis l’erreur, puis trouve un produit de matrices transposées (t()) qui soit défini et donne les nombres de la leçon, transposés.
Solution
outcome("b.matmul(&a)", b.matmul(&a));// (3, 4) x (4, 3) est défini si on transpose plutôt b : b.t() est (4, 3), a.t() est (3, 2)let c = b.t()?.matmul(&a.t()?)?;println!("{c}");// (AB)^T = B^T A^T : les mêmes nombres que le a.matmul(&b) de la leçon, transposéslet same = c.eq(&a.matmul(&b)?.t()?)?.min_all()?.to_scalar::<u8>()? == 1;== exercise 1: b x a, then b x a transposedb.matmul(&a): error: shape mismatch in matmul, lhs: [3, 4], rhs: [2, 3][[20., 56.], [23., 68.], [26., 80.], [29., 92.]]Tensor[[4, 2], f32]b.t() x a.t() equals (a x b).t(): trueUne matrice (3, 4) ne peut multiplier qu’une matrice de 4 lignes. La transposée d’un produit est le produit des transposées dans l’autre ordre, donc b.t() fois a.t(), une (4, 3) fois une (3, 2), donne la transposée (4, 2) du résultat de la leçon. eq compare élément par élément et renvoie un tenseur de u8, dont le minimum vaut 1 seulement si tous les éléments sont égaux.
- Convertis
aetbenF64,F16,BF16,U32,I64etU8avecto_dtype, et essaiematmulsur chacun. Quels types d’éléments le backend CPU sait-il multiplier ?
Solution
for dtype in [DType::F64, DType::F16, DType::BF16, DType::U32, DType::I64, DType::U8] { let result = a.to_dtype(dtype)?.matmul(&b.to_dtype(dtype)?); match result { Ok(c) => println!("{dtype:?}: ok, first row {:?}", c.to_dtype(DType::F64)?.to_vec2::<f64>()?[0]), Err(e) => println!("{dtype:?}: error: {e}"), }}== exercise 2: matmul on each dtypeF64: ok, first row [20.0, 23.0, 26.0, 29.0]F16: ok, first row [20.0, 23.0, 26.0, 29.0]BF16: error: unsupported dtype BF16 for op matmulU32: error: unsupported dtype U32 for op matmulI64: error: unsupported dtype I64 for op matmulU8: error: unsupported dtype U8 for op matmulSeulement F16, F32 et F64 : le produit matriciel du CPU appelle gemm, et cpu_backend/mod.rs, lignes 1382-1385 refuse tous les autres types. BF16, le format « brain float » dans lequel beaucoup de modèles sont stockés, doit d’abord être converti en F32 sur le CPU. Les tenseurs entiers contiennent des indices et des masques, pas des poids.
- Candle au tag
0.11.0: README,Cargo.tomldu workspace,candle-core/Cargo.toml,utils.rs - Le livre de Candle, chapitre d’installation
candle-core0.11.0 sur docs.rs- La référence Cargo : spécifier des dépendances et features
- rustc :
target-cpu