tch-rs: Thin Rust Bindings Over the libtorch C++ API
Rust bindings for the C++ api of PyTorch.
At a glance
- What is it?
- The tch crate wraps PyTorch's C++ library rather than reimplementing it, which keeps the API surface close to libtorch and makes libtorch version matching the central operational problem. It suits Rust teams embedding trained PyTorch models or writing training loops who accept a native C++ dependency.
- Who is it for?
- Adopt tch-rs if you already ship libtorch and want Rust-side tensor code, autograd through nn::VarStore, and an API that tracks the C++ one closely. Do not adopt it if you need a pure Rust dependency tree, a stable abstraction layer, or a build that works without a matching native library.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 23 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
What tch-rs Actually Wraps, and What It Refuses to Hide
The README states the goal directly: the crate provides "some thin wrappers around the C++ PyTorch api (a.k.a. libtorch)" and aims "at staying as close as possible to the original C++ api." That sentence is the whole design contract. tch-rs is not a Rust-native tensor library that happens to load PyTorch checkpoints. It is a binding layer, and the documentation says more idiomatic Rust bindings "could then be developed on top of this" rather than promising that tch itself is that layer.
The audience follows from that. If you have a Rust service that needs to run a PyTorch model, or a training loop you want to write in Rust while keeping PyTorch's autograd, tch-rs is aimed at you. If you want a dependency graph that compiles with nothing but cargo, it is not. The binding approach means the C++ library is a hard prerequisite, not an optional accelerator, and the README treats that as the first thing a reader must solve.
The Build Script Is the Real Interface
Most of the operational surface of tch-rs is not Rust code. It is the resolution order the build script follows when looking for libtorch, and the README spells out four routes.
First, a system-wide install. On Linux the build script looks for /usr/lib/libtorch.so. Second, a manual install pointed at by the LIBTORCH environment variable. Third, a Python PyTorch install, activated by setting LIBTORCH_USE_PYTORCH=1, in which case the active python interpreter is called to retrieve information about the torch package and the crate links against that version. Fourth, the download-libtorch feature, which the README says the build script can use when no system-wide libtorch is found and LIBTORCH is not set, fetching a pre-built binary. That downloaded artifact is CPU-only by default; setting TORCH_CUDA_VERSION to cu117 selects a CUDA 11.7 pre-built binary.
Two finer-grained variables exist for split layouts: LIBTORCH_INCLUDE and LIBTORCH_LIB, described as containing the include directory and the lib directory respectively. The README notes the version requirement plainly: libtorch v2.13.0. That pin is the detail most likely to bite, because the four resolution routes do not agree on which version you end up with, and a mismatch between the headers compiled against and the shared library loaded at runtime is a class of failure the Rust type system cannot catch.
Getting It Running: Commands and Environment Variables
For a manual install the README gives the shell line export LIBTORCH=/path/to/libtorch, where that path is the directory created when unzipping the downloaded archive. If headers and libraries live apart, export LIBTORCH_INCLUDE=/path/to/libtorch/ and export LIBTORCH_LIB=/path/to/libtorch/ replace it.
On Windows the README describes setting a LIBTORCH variable through Control Panel, then appending X:\path\to\libtorch\lib to Path. For a temporary session it gives the PowerShell pair: $Env:LIBTORCH = "X:\path\to\libtorch" and $Env:Path += ";X:\path\to\libtorch\lib". The README then says you should be able to run examples such as cargo run --example basics.
Static linking is opt-in through LIBTORCH_STATIC=1. The README warns that pre-compiled artifacts "don't seem to include libtorch.a by default," so this requires building PyTorch yourself. The commands it supplies clone the v2.13.0 tag with submodules, then run USE_CUDA=OFF BUILD_SHARED_LIBS=OFF python setup.py build, after which LIBTORCH points at the build directory. That is a source build of PyTorch, and the README does not pretend otherwise.
Two Windows caveats are stated outright. Debug and release builds of libtorch are not ABI-compatible, which the README says "could lead to some segfaults if the incorrect version of libtorch is used." And the MSVC Rust toolchain is recommended over MinGW, because PyTorch has compatibility issues with MinGW.
Tensors, VarStore, and Where Gradients Come From
The Rust-side API shown in the README is small. A Tensor wraps a PyTorch tensor; the basic example builds one with Tensor::from_slice(&[3, 1, 4, 1, 5]), multiplies by a scalar, and calls print().
Training is where the binding shape becomes visible. Variables are not declared with Rust types that carry gradient state. They are created through an nn::VarStore, which the README describes as creating variables "by defining their shapes and initializations." In the gradient descent example, a module built with nn::func closes over two variables created by p.zeros("x1", &[dim]) and p.zeros("x2", &[dim]), and the forward pass computes xs * &x1 + xs.exp() * &x2. The optimizer is nn::Sgd::default().build(&vs, 1e-2), and each iteration computes a loss then calls opt.backward_step(&loss).
The important structural point is that the string names passed to p.zeros are not decoration. They are the keys under which variables are registered, which is how a VarStore can later be walked, saved, or loaded. The README also shows nn::seq() with .add(nn::linear(vs / "layer1", IMAGE_DIM, HIDDEN_NODES, Default::default())) and .add_fn(|xs| xs.relu(...)) for a small MNIST network using the Adam optimizer. The path operator / builds the variable namespace the same way a filesystem path does. This mirrors libtorch's own variable registration rather than inventing a Rust abstraction over it, which is consistent with the stated goal of staying close to C++.
The Version Pin Is a Maintenance Cost, Not a Footnote
The README requires libtorch v2.13.0. That single fact drives most of the ongoing cost of using tch-rs. Upgrading PyTorch in your environment is not a cargo update; it is a coordinated change across the C++ library, the crate version, and whatever headers the build script resolved.
The resolution order makes this harder to reason about than it needs to be. A machine with /usr/lib/libtorch.so present will silently use it, ignoring LIBTORCH. A CI container with LIBTORCH_USE_PYTORCH=1 will link whatever the active python interpreter reports, which may differ from the version a developer has locally. A laptop that falls through to download-libtorch gets a CPU-only pre-built binary that nobody explicitly chose. Four routes, four possible versions, one crate. The README documents each route clearly but does not describe a mechanism for asserting that the resolved version matches the pin, so the check is left to the user.
The release history visible here does not suggest a fast-moving project. The most recent release listed is mw (model weights v0.1) dated 2022-04-02, while the last push to the default branch is 2026-08-23. That gap between tagged releases and commit activity is worth noting: the crate appears to track libtorch through ongoing commits rather than through a dense release cadence, so pinning to a crates.io version and pinning to a libtorch version are two separate decisions you have to make and reconcile yourself.
When tch-rs Is the Wrong Tool
The binding approach has a cost that shows up in deployment rather than in development. Static linking is available but the README says the pre-compiled artifacts do not include libtorch.a by default, so a single self-contained binary means compiling PyTorch from source with BUILD_SHARED_LIBS=OFF. A team that chose Rust partly to avoid complicated native build chains should weigh that carefully.
Windows deserves separate mention. The README states that debug and release libtorch builds are not ABI-compatible and that using the wrong one "could lead to some segfaults." A segfault is not a Rust panic. It bypasses unwinding, it will not be caught by Result handling, and it points at a native mismatch rather than a logic error. If your deployment target is Windows and your build pipeline mixes configurations, this is a real failure mode and not a theoretical one.
There is also a scope limitation the README implies rather than states. Because tch-rs deliberately stays close to the C++ API, it does not offer the ergonomic Rust-native modelling layer that a framework like Burn provides. Burn implements its tensor and autograd machinery in Rust, so it does not require libtorch on the build machine and can target backends including ones that do not involve PyTorch at all. The difference in approach is not a matter of polish. tch-rs gives you PyTorch's exact operator semantics and checkpoint compatibility at the price of a native dependency and version coupling. Burn gives you a Rust dependency tree and backend abstraction at the price of reimplementing operators and diverging from PyTorch's numerics. If your requirement is loading an existing PyTorch checkpoint with bit-compatible behaviour, the second option is a poor fit. If your requirement is a crate that compiles on a machine with no C++ toolchain, the first one is.
Licence and What to Verify Before Committing
The crate is Apache-2.0. That covers the Rust binding code. It does not describe libtorch, which is a separate artifact with its own terms, and it does not describe any model weights you load through it. The README's mention of an mw (model weights v0.1) release is the only signal here about weights, and it carries no licence information in the supplied material, so treat weight licensing as an open question to resolve separately. Nothing in this article is legal advice.
For a first evaluation, the concrete things to establish are: which of the four resolution routes your build actually takes, and whether that path yields v2.13.0. The system-wide route is the quietest and therefore the most likely to surprise you, since /usr/lib/libtorch.so takes precedence without any environment variable being set. If you plan to distribute binaries, test LIBTORCH_STATIC=1 early, because the README's own instructions for it involve a source build of PyTorch and that is not a step you want to discover late. On Windows, confirm the toolchain is MSVC rather than MinGW and that debug and release libtorch builds are not mixed, per the README's own warnings.
Editorial conclusion
Adopt tch-rs if you already ship libtorch and want Rust-side tensor code, autograd through nn::VarStore, and an API that tracks the C++ one closely. Do not adopt it if you need a pure Rust dependency tree, a stable abstraction layer, or a build that works without a matching native library. Before writing code, verify the exact libtorch version your build resolves to (the README pins v2.13.0), and confirm whether that resolution came from /usr/lib/libtorch.so, the LIBTORCH variable, LIBTORCH_USE_PYTORCH, or the download-libtorch feature, because those four paths produce different binaries and only one of them is reproducible on a colleague's machine.
Community notes