Ir al contenido
learn

1. Por qué Candle

Candle es un framework de aprendizaje automático escrito en Rust por Hugging Face. Te da tensores, diferenciación automática, capas de redes neuronales e implementaciones de modelos conocidos, y se compila dentro de tu programa como cualquier otro crate: sin Python, sin un runtime de C++ que instalar al lado. Esta lección lo sitúa entre las herramientas que un desarrollador C# o Java quizá ya conoce, desmonta el workspace y ejecuta un primer programa.

Lenguaje en el que escribes Qué calcula Entrenamiento Qué despliegas
PyTorch Python libtorch, en C++ y CUDA sí un entorno Python, o un modelo exportado para otro runtime
tch-rs Rust libtorch, a través de una API en C sí tu binario más las bibliotecas de libtorch
TorchSharp C#, F# libtorch, a través de paquetes nativos sí tu aplicación .NET más un paquete NuGet libtorch-*
ONNX Runtime C#, Java, Python, C++, Rust a través de ort su propio motor en C++ ante todo inferencia tu aplicación más el runtime nativo y un archivo .onnx
ML.NET C#, F# entrenadores .NET, y ONNX Runtime o TorchSharp para el aprendizaje profundo sí una aplicación .NET
DJL Java un motor que eliges: PyTorch, ONNX Runtime y otros depende del motor una aplicación JVM más las bibliotecas nativas del motor
Burn Rust backends detrás de un trait Backend, de CUDA a un backend de CPU en Rust puro sí un binario Rust
Candle Rust código Rust, con CUDA, Metal, MKL o Accelerate opcionales sí un binario Rust, o un módulo WebAssembly

Fuentes: los README de tch-rs, TorchSharp, ort, DJL, Burn y Candle.

La columna que importa para este curso es la última. Con tch-rs o TorchSharp, las cuentas con tensores ocurren en libtorch, una biblioteca nativa de varios cientos de megabytes que despliegas junto a tu programa. Con ONNX Runtime, despliegas su biblioteca nativa y un modelo exportado desde Python. El propio README de Candle plantea sus objetivos en los mismos términos: hacer posible la inferencia serverless con binarios pequeños que arrancan rápido, y sacar Python de las cargas de trabajo en producción.

Lo que pierdes es amplitud. PyTorch tiene miles de operaciones y un gran ecosistema; Candle tiene las operaciones que necesitan sus modelos. La lección 2 se topa con algunos de sus límites, como operaciones que provocan un panic sobre tensores enteros en lugar de devolver un error.

Candle se publica como varios crates. La versión estudiada aquí es la 0.11.0, la última en crates.io (26 de junio de 2026), cuyas fuentes son la etiqueta 0.11.0, commit 31f35b1. Cada enlace al código de Candle en este curso apunta a ese commit.

Crate Qué contiene Lecciones
candle-core Tensor, DType, Device, los backends de CPU, CUDA y Metal, autodiff, tensores cuantizados, los formatos safetensors y GGUF 2 a 4, 8
candle-nn capas (Linear, Embedding, normalizaciones), activaciones, pérdidas, optimizadores, VarBuilder 5, 6
candle-transformers implementaciones de modelos publicados: 125 entradas bajo models/, de BERT a Qwen3 cuantizado 7, 8
candle-datasets cargadores de conjuntos de datos habituales como MNIST 5
candle-examples 111 programas de línea de comandos, uno por modelo o tarea 7 a 9
candle-onnx un evaluador de grafos ONNX 11
candle-pyo3 bindings de Python, la dirección inversa a la de este curso —
candle-kernels, candle-metal-kernels, candle-flash-attn kernels de GPU en CUDA y Metal 9
candle-wasm-examples los mismos modelos compilados a WebAssembly para el navegador 10

candle-core activa sus backends con features de Cargo, declaradas en candle-core/Cargo.toml, líneas 48-62. Ninguna está activada por defecto, así que una dependencia sin más te da solo el backend de CPU.

Feature Qué añade Dónde
cuda el backend CUDA, a través del crate cudarc, y los kernels CUDA GPU NVIDIA, necesita el CUDA toolkit
cudnn cuda más cuDNN para las convoluciones ídem
nccl cuda más NCCL, para repartir un modelo entre varias GPU ídem
metal el backend Metal GPU de Apple, macOS
accelerate el framework Accelerate de Apple para los productos de matrices en la CPU macOS
mkl la Math Kernel Library de Intel para los productos de matrices en la CPU CPU x86

candle-nn y candle-transformers transmiten las mismas features a candle-core. La lección 9 trata las features de GPU; el código de este curso no usa ninguna.

El código del curso es un workspace de Cargo en code/candle, con un ejemplo por tema y uno por cada conjunto de soluciones de ejercicios. Necesitas Rust y un compilador de C, por una razón que se explica más abajo: en Windows bastan las Visual Studio Build Tools que pide rustup.

Ventana de terminal
cd code\candle
cargo run --release --example l01_hello
bash check.sh

check.sh ejecuta cargo fmt --check, clippy, las pruebas (incluidos los doctests compile_fail de los fragmentos que este curso muestra rechazados), luego cada ejemplo, y compara cada salida con el archivo de expected/. En Windows, ejecútalo desde Git Bash.

El libro de Candle añade candle-core con cargo add --git, que sigue la rama main. Este curso fija en cambio la versión publicada, para que las salidas de abajo sigan siendo ciertas: Cargo.toml pide exactamente la 0.11.0 con =, como describe la referencia de Cargo, y Cargo.lock está en el repositorio.

[workspace.dependencies]
candle-core = "=0.11.0"
candle-nn = "=0.11.0"

El programa es el del README de Candle, un producto de matrices, con un cambio. El README rellena las matrices con Tensor::randn, números aleatorios, y el generador aleatorio de la CPU de Candle no admite semilla (la lección 2 muestra el error), así que su salida cambia en cada ejecución. examples/l01_hello.rs usa arange en su lugar:

use candle_core::{Device, Tensor};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let device = Device::Cpu;
// arange en lugar del randn del README: los mismos números en cada ejecución y en cada SO
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]

Cuatro cosas en las que fijarse, viniendo de C# o Java:

  • Cada operación devuelve un Result. arange, reshape y matmul pueden fallar, por ejemplo con una forma que no encaja, y lo informan como un valor candle_core::Error en lugar de lanzar una excepción. El ? tras cada llamada devuelve el error a quien llama, como explica el curso de Rust. Donde TorchSharp lanza una excepción, Candle te entrega un valor.
  • El dispositivo es explícito. Cada tensor se crea en un Device: Device::Cpu aquí, un dispositivo CUDA o Metal en la lección 9.
  • La forma y el tipo de elemento son valores en tiempo de ejecución. Tensor es un único tipo para cualquier forma y cualquier tipo de elemento; la última línea de la salida imprime ambos, [2, 4] y f32. Un error de forma se descubre cuando el programa se ejecuta, no cuando compila.
  • 0f32, 6. da al primer número un sufijo de tipo; Rust infiere f32 para el segundo a partir de él.

Una matriz (2, 3) por una matriz (3, 4) da una matriz (2, 4): el primer elemento es 0·0 + 1·4 + 2·8 = 20.

Dos errores con los que se topa todo principiante

Sección titulada «Dos errores con los que se topa todo principiante»

Imprimir el resultado de matmul sin ? imprime un Result, no un tensor, y Result no implementa 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 formatter

El segundo viene de la chuleta del propio README de Candle, línea 278, que convierte un tensor con tensor.to_dtype(&DType::F16)?. to_dtype recibe el DType por valor:

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 incorrect

DType es un enum Copy pequeño, así que no hay razón para pasar una referencia; la sugerencia del compilador, quitar el &, es la corrección. Ambos fragmentos son doctests compile_fail en src/lib.rs, así que el curso se entera si una versión posterior de Candle o de Rust los acepta.

El primer cargo build --release de este workspace tardó 73 segundos y compiló 128 crates, en un Intel Core Ultra 9 285K con Windows 11 y Rust 1.94.0, limitado a 4 trabajos en paralelo (-j 4) porque la máquina era compartida. El ejecutable l01_hello ocupa 1,4 MB. Las compilaciones siguientes solo recompilan el crate del curso, en un segundo más o menos.

La mayoría de esos crates son los que cabría esperar: gemm para los productos de matrices, rayon para los hilos, half para los flotantes de 16 bits, safetensors para los archivos de modelo. Uno no. cargo tree muestra por dónde entra el compilador de C:

> cargo tree -p candle-course -e normal -i onig_sys
onig_sys v69.9.3
└── onig v6.5.3
└── tokenizers v0.22.2
└── candle-core v0.11.0

candle-core 0.11.0 depende del crate tokenizers de Hugging Face en todos los targets salvo WebAssembly (Cargo.toml, líneas 37-38), y con su feature onig, que compila la biblioteca de expresiones regulares Oniguruma desde fuentes en C. Solo la usa un archivo, quantized/tokenizer.rs, que reconstruye un tokenizador a partir de los metadatos de un archivo GGUF (lección 8). De los 119 crates detrás de candle-core, 72 están en el árbol de dependencias de tokenizers. En main, después de 0.11.0, la dependencia sigue ahí, pero con la feature fancy-regex, en Rust puro, en lugar de onig (Cargo.toml en ddf1b87), así que el requisito del compilador de C debería desaparecer con la próxima versión (por verificar cuando se publique).

examples/l01_devices.rs pregunta a la compilación qué backends tiene, y luego pide un dispositivo CUDA y uno 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}"),
}
// Recurre a la CPU en lugar de fallar
let device = Device::cuda_if_available(0)?;
cuda feature: false
metal feature: false
mkl feature: false
accelerate feature: false
Device::new_cuda(0): error: the candle crate has not been built with cuda support
Device::new_metal(0): error: the candle crate has not been built with metal support
Device::cuda_if_available(0): Cpu

La salida es la misma en una máquina con una GPU NVIDIA: utils::cuda_is_available indica si la feature cuda se compiló, no si hay una GPU. Sin la feature, Candle compila un backend sustituto cuyos constructores devuelven este error. cuda_if_available es la forma habitual de escribir un programa que usa una GPU cuando se compiló para ella, y la CPU en caso contrario.

El backend de CPU tiene un interruptor más que no es una feature. examples/l01_machine.rs imprime lo que la compilación sabe del procesador; check.sh lo ejecuta sin comparar, ya que depende de la máquina:

os / arch: windows / x86_64
compiled for avx2: false
compiled for neon: false
compiled for f16c: false
threads (rayon): 24

El procesador tiene AVX2, pero el programa no se compiló para él: Rust apunta a una CPU x86-64 de base salvo que pases -C target-cpu=native. El propio repositorio de Candle lo hace, en .cargo/config.toml, así que sus ejemplos se compilan para la máquina en la que se ejecutan, y un crate que depende de Candle no. La lección 3 mide lo que eso cambia.

Ninguno de los tres repositorios que estudia este sitio depende hoy de Candle.

  • GA ejecuta sus modelos de embeddings en C# con ONNX Runtime: GA.Business.ML.csproj referencia Microsoft.ML.OnnxRuntime 1.23.2 y Microsoft.ML.Tokenizers 2.0.0, y OnnxEmbeddingService.cs abre una InferenceSession. Es la fila “ONNX Runtime” de la tabla de arriba; la lección 7 calcula embeddings con Candle, y la lección 11 llama a un modelo de Candle desde C#.
  • TARS hace lo mismo desde F#: TarsEngine.FSharp.Cli.fsproj referencia ONNX Runtime 1.19.2 y Microsoft.ML.Tokenizers 0.22.0.
  • IX está escrito en Rust y decidió no construir sobre Candle. Su crate de diferenciación automática, ix-autograd, está escrito desde cero sobre ndarray; su documentación deja constancia de la decisión (lib.rs, línea 27) y reserva un lugar para “a future Candle or GPU backend” (tensor.rs, líneas 9-19). La lección 4 pasa el mismo gradiente por los dos.

La guía de bibliotecas Rust de IX describe Candle como “optimised for LLM inference on NVIDIA GPUs via cuTENSOR and cuDNN” (code-analysis-tools.md, línea 129). Las features CUDA del workspace de Candle piden a cudarc cuBLAS, cuBLASLt, cuRAND y NVRTC, con cuDNN como opción (Cargo.toml, líneas 50-61); cuTENSOR no está entre ellas, y no se mencionan los backends de CPU, Metal y WebAssembly. El diario lo recoge.

  • Candle es un framework Rust de tensores, autodiff y modelos, compilado dentro de tu binario. Su punto fuerte frente a tch-rs, TorchSharp y ONNX Runtime es que no hay ningún runtime nativo que desplegar.
  • candle-core contiene los tensores y los backends, candle-nn las capas y los optimizadores, candle-transformers los modelos. Los backends de GPU y MKL o Accelerate son features de Cargo opcionales.
  • Cada operación sobre tensores devuelve un Result; las formas y los tipos de elemento se comprueban cuando el programa se ejecuta.
  • Fija la versión de crates.io con = para obtener resultados reproducibles. La versión 0.11.0 necesita un compilador de C porque candle-core arrastra tokenizers con Oniguruma.
  • cuda_is_available informa de una feature compilada, no del hardware. Sin -C target-cpu=native, los kernels específicos de CPU detrás de with_avx no se compilan.

Las soluciones están en examples/l01_exercises.rs, y su salida en expected/l01_exercises.txt.

  1. En l01_hello, calcula b.matmul(&a) en lugar de a.matmul(&b). Lee el error, y luego encuentra un producto de matrices traspuestas (t()) que esté definido y dé los números de la lección, traspuestos.
Solución

Líneas 11-18:

outcome("b.matmul(&a)", b.matmul(&a));
// (3, 4) x (4, 3) está definido si trasponemos b en su lugar: b.t() es (4, 3), a.t() es (3, 2)
let c = b.t()?.matmul(&a.t()?)?;
println!("{c}");
// (AB)^T = B^T A^T: los mismos números que a.matmul(&b) de la lección, traspuestos
let same = c.eq(&a.matmul(&b)?.t()?)?.min_all()?.to_scalar::<u8>()? == 1;
== exercise 1: b x a, then b x a transposed
b.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(): true

Una matriz (3, 4) solo puede multiplicar a una matriz de 4 filas. La traspuesta de un producto es el producto de las traspuestas en el orden inverso, así que b.t() por a.t(), una (4, 3) por una (3, 2), da la traspuesta (4, 2) del resultado de la lección. eq compara elemento a elemento y devuelve un tensor de u8, cuyo mínimo es 1 solo si todos los elementos coinciden.

  1. Convierte a y b a F64, F16, BF16, U32, I64 y U8 con to_dtype, y prueba matmul con cada uno. ¿Qué tipos de elemento multiplica el backend de CPU?
Solución

Líneas 20-37:

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 dtype
F64: 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 matmul
U32: error: unsupported dtype U32 for op matmul
I64: error: unsupported dtype I64 for op matmul
U8: error: unsupported dtype U8 for op matmul

Solo F16, F32 y F64: el producto de matrices en CPU llama a gemm, y cpu_backend/mod.rs, líneas 1382-1385 rechaza cualquier otro tipo. BF16, el formato “brain float” en el que se guardan muchos modelos, hay que convertirlo antes a F32 en la CPU. Los tensores enteros contienen índices y máscaras, no pesos.