Clorch tensors are JavaCPP wrappers around LibTorch objects allocated entirely outside the JVM heap. Each wrapper is a small Java object, but it may own hundreds of megabytes — or several gigabytes — of native CPU RAM or CUDA VRAM. Getting memory management right is the single most operationally important topic in Clorch, and this page covers it completely.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.
JVM GC and Native Memory
The JVM garbage collector can eventually reclaim an unreachable Clorch tensor because JavaCPP attaches a native deallocator to every wrapper object. When the GC finalises the Java wrapper, LibTorch frees the underlying allocation. However, the GC has no visibility into:- Native heap pressure — a
[4096 4096]float32 tensor occupies 64 MB of native RAM, but the JVM sees a tiny Java object. - CUDA VRAM pressure — GPU memory exhaustion does not trigger JVM collection.
- Timing — finalisation is not guaranteed to run before the next allocation, especially under steady allocation pressure.
When to Use with-torch
The table below maps workload patterns to the right strategy:
| Workload | Recommendation |
|---|---|
| Small REPL expression or short script | GC is usually sufficient |
| Long-lived model, optimizer, or dataset | Keep it outside iteration scopes |
| Training or inference batch loop | Use one with-torch per iteration |
| Autoregressive generation or MCMC loop | Use one with-torch per step |
| Large CPU tensors | Use with-torch around temporary computation |
| CUDA workloads | Strongly prefer deterministic scopes |
| Interactive REPL session with many expressions | Use start-session! and stop-session! |
with-torch should wrap the smallest repeated unit that creates temporary tensors, not every individual operation.
Canonical Training Loop
Create the model and optimizer once before the loop. Scope only batch-local outputs, losses, and intermediates. Finish each scope with a JVM scalar ornil so no tensor escapes:
with-torch scope — they are long-lived JVM references managed by the GC. Batch intermediates (prediction, loss) are created inside the scope, and item-float extracts a plain JVM Float as the final result, so the scope releases all native allocations when the block exits.
How with-torch Works
with-torch opens a JavaCPP PointerScope. Every native pointer allocated while the scope is open is registered with that scope. Before the scope closes, Clorch calls retain! recursively on the final result — the value of the last expression — to remove it from the scope’s ownership. When the scope closes, every remaining registered pointer is immediately deallocated. The final result is returned to the caller and is now managed by the JVM GC.
Returning maps and collections works too. retain! traverses map values and collection elements, so returning a structured result is safe:
The “accidentally retaining” anti-pattern
This loop retains one tensor per iteration and then silently discards it:nil:
Explicit Retention
retain! and rescue-pointers!
When a tensor needs to escape through state that with-torch cannot traverse — an atom, a delay, a closure, a cache, or any arbitrary Java object — call retain! before the scope closes:
rescue-pointers! is an alias for retain! — use whichever name reads more clearly in context.
Simpler alternative: return from with-torch
When possible, just return the tensor as the with-torch result:
retain!.
Manual Release
release! immediately deallocates a pointer without waiting for the GC or a scope boundary. It recursively handles maps and collections:
Interactive Session Scopes
For REPL development, creating awith-torch around every expression is impractical. Instead, open a long-lived session scope for the current thread:
Forcing a GC Pass
gc! calls System/gc and System/runFinalization to prod the JVM into running pending finalizers:
Lifecycle API Reference
| Function | Effect |
|---|---|
with-torch | Opens a native pointer scope; releases all block-local pointers except those reachable from the final result |
retain! | Removes a pointer (or all pointers in a collection/map) from its current scope, handing ownership to the GC |
rescue-pointers! | Alias for retain! |
release! | Immediately and unconditionally deallocates owned pointers; pointer is invalid after this call |
start-session! | Opens a long-lived thread-local pointer scope for interactive REPL use |
stop-session! | Closes the current thread’s interactive scope and releases all its pointers |
gc! | Requests JVM GC and finalisation; diagnostic use only |
Rules of Thumb
- Keep model, optimizer, and all intentionally long-lived tensors outside iteration scopes.
- Use one
with-torchper allocating batch, generation step, or sampler step — not one per individual operation. - End a scope with a JVM scalar or
nilunless a tensor must escape; this is the simplest way to guarantee no accidental retention. - Use
retain!when a tensor must escape through an atom, closure, cache, or other side-effect thatwith-torchcannot discover. - Use
release!only when ownership is clear and unambiguous — prefer scopes. - Do not depend on
gc!for steady-state memory bounds; it is a diagnostic tool. - In REPL sessions, use
start-session!/stop-session!rather than sprinklingwith-torcharound every expression.