
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.TorchSharppackage 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()vsforward():call()runs any registered hooks and thenforward(). Prefercall()from outside the module.backward(): walks the computation graph in reverse and fills in.gradon 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.pyscript from the TorchSharp repository to export a Pythonstate_dictinto TorchSharp's format. - Use the community
TorchSharp.PyBridgeNuGet 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-cpuand 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 inno_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 needsint64. - 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()andno_grad(), which gives noisy results and wastes memory. - Trying to load a pickled
.ptfile withmodel.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, andmodel.eval()plustorch.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.
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.
Comments
Post a Comment