Skip to main content

TorchSharp Tutorial: Deep Learning in C# with PyTorch

Learn TorchSharp in C#: build, train, and deploy PyTorch neural networks in .NET with GPU support. Follow the tutorial and start coding today.

If you're a .NET developer who wants deep learning without leaving C#, TorchSharp is the most direct way to get it. TorchSharp is a .NET library that exposes PyTorch's native engine (libtorch) to C#. You get tensors, automatic differentiation, GPU acceleration, and the same nn.Module style of building models that Python developers use. This TorchSharp tutorial takes you from installation to a trained neural network. Along the way it explains why the library works the way it does and covers the mistakes that catch most C# developers out.

You might be a beginner searching "how to do deep learning in C#", an intermediate developer looking for TorchSharp best practices, or a senior engineer deciding whether PyTorch in C# is ready for production. Either way, this guide has what you need.

What Is TorchSharp and Why Use It for Deep Learning in C#?

TorchSharp is an open-source project under the .NET Foundation (dotnet/TorchSharp on GitHub). It is not a reimplementation of PyTorch. It is a thin, strongly typed wrapper over the same C++ library that powers PyTorch in Python. The tensor math, the autograd engine, and the CUDA kernels are the same code Python uses.

That design has some useful consequences:

  • Performance matches PyTorch for heavy work, because the heavy work runs in native code.
  • The API mirrors PyTorch on purpose. You'll see torch.randn, nn.Linear, optimizer.zero_grad(), and even snake_case method names. That's a deliberate choice: most PyTorch tutorials, papers, and Stack Overflow answers carry over to C# with only small syntax changes.
  • You stay in one ecosystem. Training, inference, ASP.NET Core APIs, background workers, and desktop apps can all share a single C# codebase, with no Python sidecar.
  • ML.NET uses it internally. The Microsoft.ML.TorchSharp package runs ML.NET's text classification, named entity recognition, and object detection trainers on top of TorchSharp.

TorchSharp is the right tool when you need custom neural networks (your own architectures, loss functions, and training loops) inside .NET. If you only need to run a model that someone already trained, ONNX Runtime is often simpler. More on that in the best practices section.

How to Install TorchSharp in a .NET Project

TorchSharp comes in two parts: the managed API and a native libtorch backend. The backend packages are big (the CUDA ones run to several gigabytes), so you pick the one that fits your hardware:

  • TorchSharp-cpu: CPU only, works on Windows, Linux, and macOS.
  • TorchSharp-cuda-windows: NVIDIA GPU support on Windows.
  • TorchSharp-cuda-linux: NVIDIA GPU support on Linux.
  • TorchSharp: the managed API only, for when you supply libtorch yourself.

Create a console app and add the CPU package to get started:

// Terminal
// dotnet new console -n TorchSharpDemo
// cd TorchSharpDemo
// dotnet add package TorchSharp-cpu

using TorchSharp;
using static TorchSharp.torch;

Console.WriteLine($"CUDA available: {torch.cuda.is_available()}");

var a = torch.randn(2, 3);
var b = torch.ones(2, 3);
var c = a + b;

Console.WriteLine(c.ToString(TensorStringStyle.Numpy));

The using static TorchSharp.torch; line is what lets your C# read almost exactly like Python PyTorch. Without it you'd have to write torch. in front of everything.

TorchSharp Tensors: The Foundation of Every Model

A tensor is an n-dimensional array that can live on the CPU or the GPU and can record the operations applied to it, so gradients can be computed later. Almost everything in a deep learning program is tensor manipulation.

using TorchSharp;
using static TorchSharp.torch;

// Create tensors from C# arrays
float[] raw = { 1f, 2f, 3f, 4f, 5f, 6f };
var t = torch.tensor(raw).reshape(2, 3);   // shape [2, 3], dtype float32

Console.WriteLine($"Shape: [{string.Join(", ", t.shape)}], dtype: {t.dtype}");

// Element-wise math and matrix multiplication
var doubled = t * 2;
var product = t.matmul(t.T);               // [2,3] x [3,2] = [2,2]

// Autograd: track operations to compute gradients
var w = torch.tensor(3.0f, requiresGrad: true);
var loss = (w * w) + (2 * w);              // d(loss)/dw = 2w + 2 = 8
loss.backward();
Console.WriteLine($"Gradient: {w.grad.item<float>()}"); // 8

// Move to GPU when one is available
var device = torch.cuda.is_available() ? torch.CUDA : torch.CPU;
var onDevice = t.to(device);

Why the dtype matters: if you create a tensor from a C# double[], you get float64. Neural network layers default to float32, so passing float64 input produces a dtype mismatch error. Use float[] for your data unless you really need double precision.

Your First Neural Network in C#: Linear Regression

Let's train the smallest possible "neural network": one linear layer that learns y = 3x + 2 from noisy data. This example covers the full training loop, and that loop is the same whether your model has 2 parameters or 2 billion.

using TorchSharp;
using static TorchSharp.torch;
using static TorchSharp.torch.nn;

torch.manual_seed(42);
var device = torch.cuda.is_available() ? torch.CUDA : torch.CPU;

// Synthetic data: y = 3x + 2 + noise
var x = torch.rand(1000, 1, device: device);
var y = x * 3 + 2 + torch.randn(1000, 1, device: device) * 0.1f;

var model = Linear(1, 1).to(device);
var optimizer = torch.optim.SGD(model.parameters(), 0.1);
var lossFn = MSELoss();

for (int epoch = 1; epoch <= 1000; epoch++)
{
    // Every tensor created inside this scope is freed when the scope ends
    using var scope = torch.NewDisposeScope();

    optimizer.zero_grad();                 // 1. clear old gradients
    var prediction = model.call(x);        // 2. forward pass
    var loss = lossFn.call(prediction, y); // 3. compute loss
    loss.backward();                       // 4. backpropagate
    optimizer.step();                      // 5. update weights

    if (epoch % 200 == 0)
        Console.WriteLine($"Epoch {epoch}: loss = {loss.item<float>():F5}");
}

Console.WriteLine($"Learned weight: {model.weight.item<float>():F3} (expected 3)");
Console.WriteLine($"Learned bias:   {model.bias.item<float>():F3} (expected 2)");

Why each step of the training loop matters

  • zero_grad(): PyTorch adds new gradients to the old ones by default. Skip this call and each step uses the sum of every previous gradient, so training diverges.
  • call() vs forward(): call() runs any registered hooks and then forward(). Prefer call() from outside the module.
  • backward(): walks the computation graph in reverse and fills in .grad on each parameter.
  • step(): applies the optimizer's update rule (here plain SGD) using those gradients.

Building a Custom Neural Network Module in C#

Real models are custom classes that inherit from Module<TInput, TOutput>. The generic parameters are a TorchSharp addition that gives you compile-time type safety Python doesn't have.

using TorchSharp;
using static TorchSharp.torch;
using static TorchSharp.torch.nn;

public sealed class Classifier : Module<Tensor, Tensor>
{
    private readonly Module<Tensor, Tensor> fc1;
    private readonly Module<Tensor, Tensor> fc2;
    private readonly Module<Tensor, Tensor> fc3;
    private readonly Module<Tensor, Tensor> dropout;

    public Classifier(int inputs, int hidden, int classes)
        : base(nameof(Classifier))
    {
        fc1 = Linear(inputs, hidden);
        fc2 = Linear(hidden, hidden);
        fc3 = Linear(hidden, classes);
        dropout = Dropout(0.2);

        // CRITICAL: registers fields as submodules so their parameters
        // are visible to the optimizer, .to(device), save() and load().
        RegisterComponents();
    }

    public override Tensor forward(Tensor input)
    {
        using var h1 = functional.relu(fc1.call(input));
        using var d1 = dropout.call(h1);
        using var h2 = functional.relu(fc2.call(d1));
        return fc3.call(h2); // raw logits; CrossEntropyLoss applies softmax
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            fc1.Dispose(); fc2.Dispose(); fc3.Dispose(); dropout.Dispose();
            ClearModules();
        }
        base.Dispose(disposing);
    }
}

Why RegisterComponents() is essential: in Python, PyTorch hooks attribute assignment to discover submodules automatically. C# has no equivalent mechanism, so TorchSharp uses reflection over your fields when you call RegisterComponents(). Leave it out and model.parameters() comes back empty. The optimizer then has nothing to update, and your loss stays flat with no error to explain why. It's the most common TorchSharp bug.

For simple stacks you can skip the custom class and use Sequential:

var mlp = Sequential(
    ("fc1",   Linear(20, 64)),
    ("relu1", ReLU()),
    ("fc2",   Linear(64, 2))
);

Training a Classifier with Mini-Batches (TorchSharp Best Practices)

Here is a complete, runnable training program. It uses shuffled mini-batches, the Adam optimizer, separate train and eval modes, and measures accuracy. These are the patterns you'll use in real projects.

using TorchSharp;
using static TorchSharp.torch;
using static TorchSharp.torch.nn;

torch.manual_seed(1);
var device = torch.cuda.is_available() ? torch.CUDA : torch.CPU;

// Synthetic dataset: 20 features, label = 1 if sum of features > 0
const int samples = 5000, features = 20, batchSize = 64;
var X = torch.randn(samples, features, device: device);
var Y = (X.sum(1) > 0).to(torch.int64);  // CrossEntropyLoss needs int64 labels

var model = new Classifier(features, 64, 2).to(device);
var optimizer = torch.optim.Adam(model.parameters(), 1e-3);
var lossFn = CrossEntropyLoss();

for (int epoch = 1; epoch <= 10; epoch++)
{
    model.train(); // enables dropout
    double totalLoss = 0;
    int batches = 0;

    using (var epochScope = torch.NewDisposeScope())
    {
        var perm = torch.randperm(samples, device: device);

        for (int start = 0; start < samples; start += batchSize)
        {
            using var batchScope = torch.NewDisposeScope();

            int len = Math.Min(batchSize, samples - start);
            var idx = perm.narrow(0, start, len);
            var xb = X.index_select(0, idx);
            var yb = Y.index_select(0, idx);

            optimizer.zero_grad();
            var loss = lossFn.call(model.call(xb), yb);
            loss.backward();
            optimizer.step();

            totalLoss += loss.item<float>();
            batches++;
        }
    }

    // Evaluation: no dropout, no gradient tracking
    model.eval();
    using (torch.no_grad())
    using (var evalScope = torch.NewDisposeScope())
    {
        var predictions = model.call(X).argmax(1);
        var correct = predictions.eq(Y).sum().item<long>();
        Console.WriteLine(
            $"Epoch {epoch}: loss {totalLoss / batches:F4}, " +
            $"accuracy {100.0 * correct / samples:F2}%");
    }
}

Why model.train(), model.eval() and no_grad() matter

Layers like Dropout and BatchNorm behave differently during training and inference. If you forget model.eval(), dropout keeps randomly zeroing activations at inference time, so your predictions become noisy and less accurate. torch.no_grad() tells autograd not to build a computation graph. That cuts memory use sharply and speeds up inference. Use both whenever you're not training.

For bigger datasets, TorchSharp includes torch.utils.data.Dataset and DataLoader, plus TorchSharp.torchvision with datasets such as MNIST and CIFAR-10 and image transforms. The loop structure stays exactly the same.

Memory Management: The #1 TorchSharp Pitfall

This is where C# developers get caught out, so it gets its own section. Tensors wrap native memory the .NET garbage collector can't see. A 100 MB GPU tensor looks like a few bytes of managed object to the GC, so the GC feels no pressure to collect it. In a long training loop that means your GPU or RAM fills up and you get out-of-memory crashes, even though the .NET heap looks tiny.

TorchSharp gives you two tools for this:

  • using var t = ...: deterministic disposal of a single tensor.
  • torch.NewDisposeScope(): every tensor created inside the scope is disposed when the scope ends. This is the recommended approach for training loops.

If a function creates a tensor inside a scope and needs to return it, move the tensor out explicitly:

static Tensor Normalize(Tensor input)
{
    using var scope = torch.NewDisposeScope();
    var mean = input.mean();
    var std = input.std();
    var result = (input - mean) / std;   // intermediates are freed with the scope
    return result.MoveToOuterDisposeScope(); // result survives
}

Don't call GC.Collect() as a workaround. It's slow and unreliable. Dispose scopes make memory use predictable, and predictable memory is what production workloads need.

Saving, Loading, and Using PyTorch Models from .NET

Saving TorchSharp models

// Save trained weights
model.save("classifier.dat");

// Later (or in another app): build the same architecture, then load weights
var restored = new Classifier(20, 64, 2);
restored.load("classifier.dat");
restored.eval();

Important: TorchSharp's native save() format is not the same as PyTorch's .pt/pickle format. You have a few options for moving weights between Python and .NET:

  • Use the exportsd.py script from the TorchSharp repository to export a Python state_dict into TorchSharp's format.
  • Use the community TorchSharp.PyBridge NuGet package, which adds extension methods for loading and saving PyTorch-format weights and safetensors.
  • Load a TorchScript model exported from Python:
// Python side: torch.jit.script(model).save("model.pt")
var scripted = torch.jit.load<Tensor, Tensor>("model.pt");
scripted.eval();

using (torch.no_grad())
{
    var output = scripted.call(torch.randn(1, 20));
    Console.WriteLine(output.ToString(TensorStringStyle.Numpy));
}

This hybrid workflow is common in industry. Data scientists train in Python, and .NET engineers load the model in C# services. TorchSharp lets both teams stay on the same PyTorch runtime.

Advanced TorchSharp in C#: Production Tips for Senior Developers

  • Pick one backend package. Referencing both TorchSharp-cpu and a CUDA package, or mismatched versions, causes native loading errors. Keep the managed and native versions in sync.
  • Watch Docker image size. CUDA libtorch is huge. Use multi-stage builds, and consider CPU-only images for inference services that don't need a GPU.
  • Load models once. In ASP.NET Core, register a loaded, eval()-mode model as a singleton. Don't load it per request. Wrap each inference call in no_grad() and a dispose scope, and load-test concurrent access before shipping.
  • Consider ONNX Runtime for inference-only workloads. For deployment-only scenarios it's lighter and runs on more kinds of hardware. TorchSharp shines when you need training, fine-tuning, or custom autograd.
  • Seed everything with torch.manual_seed() for reproducible experiments. Note that some CUDA operations are non-deterministic even with a fixed seed.
  • Check torch.cuda.is_available() at startup and log the device you picked. A service silently running on the CPU is a common cause of performance problems.

Common TorchSharp Mistakes (Quick Checklist)

  • Forgetting RegisterComponents(), so the model never learns.
  • Not disposing tensors, which leads to GPU or RAM out-of-memory errors.
  • Using double[] input and getting float64 vs float32 errors.
  • Passing float labels to CrossEntropyLoss. It needs int64.
  • Keeping the model and data on different devices, which throws "expected all tensors to be on the same device".
  • Skipping zero_grad(), so gradients pile up and training diverges.
  • Running inference without eval() and no_grad(), which gives noisy results and wastes memory.
  • Trying to load a pickled .pt file with model.load(). Use TorchScript, exportsd.py, or PyBridge instead.

Conclusion: Is TorchSharp Right for Deep Learning in C#?

TorchSharp gives .NET developers real PyTorch power: the same native engine, the same GPU kernels, and an API close enough to Python that the world's PyTorch learning material still applies to you. You can now install it, work with tensors and autograd, build custom modules, train with mini-batches, manage native memory, and share models with Python teams.

Key takeaways:

  • TorchSharp wraps libtorch, so performance matches PyTorch for heavy workloads.
  • Always call RegisterComponents() in custom modules.
  • Use torch.NewDisposeScope() in every training and inference loop, because the GC can't see native tensor memory.
  • Use model.train() for training, and model.eval() plus torch.no_grad() for inference.
  • Use TorchScript, exportsd.py, or TorchSharp.PyBridge to move models between Python and C#.
  • Choose ONNX Runtime for deployment-only scenarios, and TorchSharp when you need training or custom architectures.

Start with the linear regression example, swap in your own data, and build up from there. Deep learning in C# is production-ready, and TorchSharp is how you do it.

About csharp-coder.com
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.

Comments

Popular posts from this blog

.NET MAUI Tutorial 2026: Build Cross-Platform Apps in C#

Learn .NET MAUI in 2026 to build iOS, Android, Windows & Mac apps from one C# codebase. Start this cross-platform tutorial with code examples today. .NET MAUI (Multi-platform App UI) is Microsoft's framework for building native iOS, Android, Windows, and macOS apps from a single C# codebase . If you've ever wanted to ship a mobile app without learning Swift, Kotlin, and Win32 separately, this .NET MAUI tutorial for 2026 is your starting point. In this guide you'll learn what .NET MAUI is, why it matters for cross-platform app development in C#, and how to build your first working app — with runnable code examples and the best practices senior engineers actually use in production. What Is .NET MAUI and Why Use It in 2026? .NET MAUI is the evolution of Xamarin.Forms, fully integrated into the modern .NET runtime. With one project and one language — C# — you target four platforms. The framework compiles to native UI controls on each device, so a button on iOS...

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...