Toro

PyTorch semantics, idiomatic F#. Powered by TorchSharp.

Toro wraps TorchSharp with an F#-idiomatic API. It keeps PyTorch's tensor semantics while using records, computation expressions, and direct method chaining.

  • Direct API -- operations throw on failure for concise method chaining, just like PyTorch.
  • Records as models -- models are plain F# records. Model.trainableVars collects parameters by reflection, so there is no base class to inherit.
  • Ownership management -- scoped { } CE automatically disposes intermediate tensors. Return values are kept alive past the scope.
  • Computation expressions for composition -- sequential { } composes layers; pipeline { } enables composition for heterogeneous signatures.
  • PyTorch-compatible surface -- tensor operations, neural network layers, and training utilities mirror PyTorch naming and behavior.
PackagePurpose
ToroTensors, DType, Device, SafeTensors I/O
Toro.NNLayers, loss functions, optimizers, metrics
Toro.GNNGraph neural network layers and batching
Toro.VisionImage I/O and transforms (SkiaSharp + Torch)
Toro.TextTokenization via Microsoft.ML.Tokenizers
Toro.HubHugging Face Hub client for pre-trained models

Install

dotnet add package Toro
dotnet add package Toro.NN
dotnet add package TorchSharp-cpu

# Optional domain packages
dotnet add package Toro.GNN      # Graph neural networks
dotnet add package Toro.Vision   # Image transforms
dotnet add package Toro.Text     # Tokenization and text preprocessing

Train an XOR model

open Toro
open Toro.NN

let x = Tensor.ofList ([ [0f;0f]; [0f;1f]; [1f;0f]; [1f;1f] ], Cpu)
let y = Tensor.ofList ([ [0f]; [1f]; [1f]; [0f] ], Cpu)

let l1 = Linear.init 2 16 F32 Cpu
let l2 = Linear.init 16 1 F32 Cpu
let model = sequential { l1; Relu; l2 }

let opt = AdamW.createWithLr 0.01 (Model.trainableVars model)
for _ in 1..500 do
    scoped {
        opt.zeroGrad ()
        let pred = model.forward x
        let loss = Loss.mse pred y
        loss.backward ()
        opt.step ()
    }

Operations throw on failure. scoped { } disposes intermediate tensors at the end of each iteration.

Define a CNN with records

Models are plain F# records. Each part defines a forward member:

type Features = {
    Conv1: Conv2d; Bn1: BatchNorm; Pool1: MaxPool2d
    Conv2: Conv2d; Bn2: BatchNorm; Pool2: MaxPool2d
} with
    member this.forward(train, x) =
        x
        |> this.Conv1.forward
        |> this.Bn1.forwardT train
        |> _.relu()
        |> this.Pool1.forward
        |> this.Conv2.forward
        |> this.Bn2.forwardT train
        |> _.relu()
        |> this.Pool2.forward

type Classifier = { Fc1: Linear; Drop: Dropout; Fc2: Linear } with
    member this.forward(train, x) =
        x
        |> _.flatten(1, -1)
        |> this.Fc1.forward
        |> _.relu()
        |> this.Drop.forwardT train
        |> this.Fc2.forward

type CnnModel = { Features: Features; Classifier: Classifier } with
    member this.forward(train, x) =
        x |> this.Features.forward(train) |> this.Classifier.forward(train)

Nested records compose naturally. Model.trainableVars walks the entire structure by reflection.

Evaluate

Toro.noGrad (fun () ->
    let pred = model.forward testX
    let argmax = pred.argmax 1
    let eq = (argmax .=. testY).toDType F32
    let acc = eq.meanAll ()
    printfn "Accuracy: %.1f%%" (acc.item () * 100.0)
)

Examples

ExampleDescription
LinearRegressionGradient descent with raw tensors
SimpleTrainingXOR with sequential { } CE
MnistTrainingCNN image classification
MnistCnnCNN with BatchNorm, Dropout
MnistAutoencoderAutoencoder with image output
MnistGanGAN image generation
CharRnnCharacter-level text generation with LSTM
TextClassifierTransformer-based text classification
SimpleGcnGNN node classification with GCNConv
HubSentimentLoad DistilBERT from Hugging Face Hub for sentiment analysis