Clorch executes tensor operations in C++ through LibTorch, bypassing the JVM for all heavy numerical compute. The JVM is responsible for orchestration — building computation graphs, dispatching operations, and managing control flow — while actual matrix multiplications, convolutions, and reductions run entirely in native code. Understanding where the JVM boundary sits is the key to writing fast Clorch programs and diagnosing unexpected slowdowns or memory growth.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/antlobach/clorch/llms.txt
Use this file to discover all available pages before exploring further.
Performance Design
Native C++ Execution
When you call(t/add tensor 1.0), Clorch invokes a native LibTorch operation through JavaCPP’s JNI bridge. The JVM does not touch the tensor data; the operation executes in the same C++ runtime that backs PyTorch. This means:
- Matrix multiplications on CPU use BLAS/MKL/OpenBLAS (whichever LibTorch is linked against).
- CUDA operations execute asynchronously on GPU kernels.
- Autograd graph construction is handled in C++.
Vectorized Operations vs Clojure Loops
The most common performance mistake is iterating over tensor elements in Clojure instead of using vectorized tensor operations. Each iteration of a Clojure loop that touches individual tensor elements crosses the JNI boundary, defeating the purpose of native execution.Contiguous Tensors and view
Some shape manipulation operations like t/view require the tensor to be contiguous in memory. After operations like t/transpose or t/ix (slicing), a tensor may become non-contiguous — its elements are valid but laid out with non-unit strides.
"input tensor must be contiguous", add .contiguous before the view call.
Using autograd/no-grad for Inference Speed
During inference, the autograd engine builds a computation graph to support backward. When you only need forward-pass results, disable graph construction with autograd/no-grad. This eliminates significant memory allocation and overhead.
nn/generate wraps its entire generation loop in autograd/no-grad automatically. Wrap your own evaluation and generation code similarly.
JNI Overhead Guidance
Each JNI call has a small but non-zero fixed cost (typically tens of microseconds). This cost is negligible when amortized over large tensors, but it compounds when you make many small calls in a tight loop.Batch Your Operations
Keep Long-Lived Objects Outside Loops
Models, optimizers, and large tensors that persist across iterations should be created once outside the training loop. Creating them insidewith-torch scopes or loop bodies triggers unnecessary native allocation and deallocation.
Memory Profiling
LibTorch allocates tensors in native (non-JVM) memory. The JVM garbage collector cannot see or apply pressure to this memory. RSS (Resident Set Size) is the correct metric for tracking total process memory — it includes both JVM heap and native allocations.Using the Built-in Profiler Script
Clorch includes a profiling script that monitors JVM heap and native RSS simultaneously:Manual RSS Monitoring
Useps to track the RSS of your Clojure process:
/proc/<PID>/status for more detail:
Diagnosing RSS Growth
If RSS grows steadily while the JVM heap remains stable, you likely have a native memory leak. The two most common causes:Missing with-torch Scope
Tensors allocated inside a training loop accumulate if no with-torch scope releases them. Wrap each batch’s allocations:
with-torch. If a tensor must escape the scope, call t/retain! on it explicitly before the scope closes, then t/release! when you are done with it.
Leaked Native Pointers
Long-lived REPL sessions can accumulate tensors bound to Vars or atoms that are never released. Periodically check that your atoms anddef values do not hold references to tensors that should have been freed.
Performance Checklist
Quick performance review
Quick performance review
| Concern | Check |
|---|---|
| Vectorization | Are you using tensor ops instead of element-wise Clojure loops? |
| No-grad at inference | Is every evaluation loop wrapped in autograd/no-grad? |
| Contiguous memory | Do you call .contiguous before view after transpose/slice? |
| Long-lived objects | Are models and optimizers defined outside the training loop? |
| Batch operations | Are you grouping small tensor calls rather than calling one at a time? |
| Memory scopes | Is each batch’s allocation wrapped in t/with-torch? |
| RSS stability | Does RSS stabilize after the first epoch in a multi-epoch run? |