بنقرة واحدة
torch-tensors
Nim bindings to libtorch for tensor operations with high-level sugar
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Nim bindings to libtorch for tensor operations with high-level sugar
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
Nim testing conventions, unittest framework, and C++ compatibility patterns
Nim type system patterns and pitfalls
Common import patterns and pitfalls for the Tattletale Nim project
Regex functionality in Nim including std/re, std/nre wrappers around PCRE, and the pure Nim nim-regex alternative with linear-time matching guarantees
Nim to Python interoperability including nimpy for calling Python from Nim and exporting Nim to Python, nimporter for packaging Nim modules as Python packages, and cffi/ctypes for calling Nim from Python
Nim's hash table module for key-value storage
| name | torch_tensors |
| description | Nim bindings to libtorch for tensor operations with high-level sugar |
| license | MIT |
| compatibility | opencode |
| metadata | {"audience":"ml-developers","workflow":"tensor-computing"} |
Torch tensors provide bindings to PyTorch's libtorch C++ library:
torch_tensors.nim (raw FFI to C++)torch_tensors_sugar.nim (Nim-friendly API)Use torch tensors when you need to:
type TorchTensor* {.importcpp: "torch::Tensor", cppNonPod, bycopy.} = object
type ScalarKind* {.importc: "torch::ScalarType", size: sizeof(int8).} = enum
kUint8 = 0
kInt8 = 1
kInt16 = 2
kInt32 = 3
kInt64 = 4
kFloat16 = 5
kFloat32 = 6
kFloat64 = 7
kComplexF32 = 9
kComplexF64 = 10
kBool = 11
kBfloat16 = 15
type DeviceKind* {.importc: "c10::DeviceType", size: sizeof(int16).} = enum
kCPU = 0
kCUDA = 1
# ... other devices
type Device* {.importc: "c10::Device", bycopy.} = object
kind: DeviceKind
index: DeviceIndex
# From blob (no copy, shares memory)
let tensor = from_blob(data_ptr, sizes, dtype)
# Empty tensor
let tensor = empty(size, dtype)
# Clone tensor
let tensor2 = tensor.clone()
# From existing data
let tensor = toTorchTensor(my_seq)
func toScalarKind*(T: typedesc[SomeTorchType]): static ScalarKind
func toTypedesc*(scalarKind: ScalarKind): typedesc
# Example: Convert between Nim types and Torch ScalarKind
let dtype = int32.toScalarKind()
let nimType = kFloat32.toTypedesc() # Returns typedesc[float32]
Torch uses ArrayRef[T] for shape/size parameters. These templates convert between Nim openArrays and Torch ArrayRef:
# Convert Nim openArray to Torch ArrayRef (for shape/size parameters)
template asTorchView*[T](oa: openArray[T]): ArrayRef[T]
template asTorchView*(meta: Metadata): ArrayRef[int64]
# Convert Torch ArrayRef back to Nim openArray
template asNimView*[T](ar: ArrayRef[T]): openArray[T]
# Example: Creating tensors with shape
let shape = @[3, 4, 5].asTorchView()
let tensor = empty(shape, kFloat32)
# Reading tensor shape
let sizes = tensor.sizes() # Returns IntArrayRef
echo sizes.asNimView() # @[3, 4, 5]
# ArrayRef helpers
let len = sizes.len() # 3
for val in sizes.items(): # Iterate values
echo val
let first = sizes[0] # Index access
Note: asTorchView creates a temporary copy because ArrayRef requires a pointer to data.
let dim = tensor.dim()
let shape = tensor.sizes() # Returns IntArrayRef
let strides = tensor.strides()
let ndim = tensor.ndimension()
let numel = tensor.numel() # Total elements
let nbytes = tensor.nbytes() # Bytes size
# Raw data pointer
let data_ptr = tensor.data_ptr(float32)
# Get scalar from 0-dim tensor
let value: float32 = tensor.item(float32)
# Get value at index
let value = tensor[0, 1]
# Set value at index
tensor[0, 1] = 42.0
# Inclusive range (end included) - rarely used
let slice = tensor[0..5, 3] # elements 0,1,2,3,4,5
# Exclusive range (end excluded) - matches Python semantics
let slice = tensor[0..<5, 3] # elements 0,1,2,3,4
# Span slice (whole dimension, equivalent to Python ":")
let span = tensor[_, 3] # all of dimension 0, index 3 of dimension 1
# Full span (all dimensions)
let all = tensor[_.._] # all dimensions, equivalent to Slice()
# With step
let stepped = tensor[0..10|2]
- (Python-style)IMPORTANT: The syntax uses -N (negative numbers) following Python conventions, NOT ^N (hat notation).
Negative indexing rules:
-1 = last element (excludes last in slicing)-2 = second-to-last element-3 = third-to-last elementCombined with ..- for end-relative slicing:
# Python equivalents:
# a[:-1] -> all but last element
# a[-3:] -> last 3 elements
# a[-3:-1] -> elements from 3rd-from-end to before last
# Nim equivalents (using - for negative, ..- for end-relative):
let slice = tensor[_..-1, _] # all but last (Python: a[:-1])
let slice = tensor[-3.._, _] # last 3 elements (Python: a[-3:])
let slice = tensor[-3..-1, _] # 3rd-from-end to before last (Python: a[-3:-1])
# Combined with start index:
let slice = tensor[1..-1, _] # from index 1 to before last (Python: a[1:-1])
# From negative start to end (use _ for "to end"):
let slice = tensor[-4.._] # Python: a[-4:] - last 4 elements (all remaining dims)
let slice = tensor[-4.._, _] # Python: a[-4:, :] - same, explicit for dim 1
- instead of ^The change from ^ (Nim inclusive) to - (Python exclusive) was intentional:
| Old (^) | New (-) | Reason |
|---|---|---|
| Exclusive mental model | Matches Python exactly | Easier to port Python algorithms |
| Off-by-one confusion | No off-by-one | Just subtract 1 from negative index |
a[^1..^2] | a[-1..-2] | Direct Python translation |
Python to Nim translation cheat sheet:
a[:-1] → a[_..-1] (subtract 1 from stop)a[-3:] → a[-3.._] (use _ for "to end")a[-3:-1] → a[-3..-1] (stop is exclusive, so -1 means "before last")_) vs Ellipsis (...)The _ symbol and ... ellipsis have different meanings:
_ (Span): Selects the entire dimension. Equivalent to Python's : or libtorch's Slice().
tensor[_, 3] = tensor[:, 3] = all of dim 0, specific index of dim 1tensor[_.._] = tensor[:, :] = all dimensions fully selected... (Ellipsis): Expands to fill remaining dimensions. Equivalent to Python's ... and libtorch's torch::indexing::Ellipsis.
tensor[..., 0] = tensor[:, :, :, 0] = ellipsis expands to match tensor ranktensor[0, ...] = tensor[0, :, :, :] = leading index, ellipsis fills resttensor[1, ..., 0] = tensor[1, :, :, 0] = indices at start and end, ellipsis in middle# Assign single value
tensor[0..5, 3] = 999.0
# Assign array values
tensor[0..1, 0..1] = [[111, 222], [333, 444]]
# Assign from another tensor
tensor[-2..-1, 2..<5] = other_tensor # last 2 elements, cols 2-4
let c = a + b
let d = a - b
let e = a * b
let f = a * 2.0
a += b
a -= 5.0
let reshaped = tensor.reshape([3, 4])
let transposed = tensor.transpose(0, 1)
let permuted = tensor.permute([2, 0, 1])
let sum = tensor.sum()
let mean = tensor.mean()
let max_val = tensor.max()
let min_val = tensor.min()
# Change dtype
let float_tensor = int_tensor.to(kFloat32)
# Change device
let cpu_tensor = gpu_tensor.cpu()
let gpu_tensor = cpu_tensor.cuda()
let rand_tensor = rand([3, 4]) # Uniform [0, 1)
let randn_tensor = randn([3, 4]) # Normal N(0, 1)
let zeros_tensor = zeros([3, 4])
let ones_tensor = ones([3, 4])
Important notes:
from_blob creates views sharing original memoryclone() creates an independent copy.contiguous() before direct data accesstry:
check_index(tensor, idx0, idx1)
let value = tensor[idx0, idx1]
except IndexDefect as e:
echo "Out of bounds: ", e.msg
Convert Nim sequences to Torch ArrayRef for shape parameters:
proc generateExpectedTensor*(pattern: string, shape: seq[int64], dtype: ScalarKind): TorchTensor =
let shapeRef = shape.asTorchView()
let numel = shape.product()
case pattern
of "gradient":
arange(numel, dtype).reshape(shapeRef).to(dtype)
of "alternating":
let flat = arange(numel, kInt64)
let modVal = (flat % 2).to(kFloat64)
modVal.reshape(shapeRef).to(dtype)
else:
raise newException(ValueError, "Unknown pattern: " & pattern)
Convert between Dtype enum and ScalarKind:
const TestedDtypes = [F64, F32, F16, I64, I32, I16, I8, U64, U32, U16, U8]
proc runTests*() =
for dtype in TestedDtypes:
let torchType = dtype.toTorchType()
let tensor = arange(8, torchType)
check tensor.scalarType() == torchType
Create expected tensors and compare with loaded ones:
proc compareTensors*(expected, actual: TorchTensor) =
check expected.shape == actual.shape
check expected.scalarType() == actual.scalarType()
check actual == expected # Uses equal() under the hood
# Usage in tests
let expectedTensor = generateExpectedTensor(pattern, shape, dtype.toTorchType())
let actualTensor = safetensorsLoader.getTensor(key)
check actualTensor == expectedTensor
Many tensor operations can be chained:
let tensor = arange(numel, kInt64)
.reshape(shapeRef)
.to(kFloat64)
.cpu()
The indexing macros provide Python-like slicing with Nim syntax:
# Basic slices (inclusive start, exclusive end like Python)
let slice = tensor[0..5] # elements 0,1,2,3,4
let slice = tensor[1..3, 0..2] # 2D slice
# Exclusive end with ..<
let slice = tensor[0..<5] # elements 0,1,2,3,4 (same as 0..4)
# Negative indexing with - (Python-style, EXCLUSIVE upper bound)
# -1 = last element, -2 = second-to-last, etc.
let slice = tensor[-2..-1] # last 2 elements (Python: a[-2:])
let slice = tensor[0..-1] # all but last element (Python: a[:-1])
let slice = tensor[0..-3] # all but last 2 elements (Python: a[:-3])
# Stepped slices with |
let slice = tensor[0..10|2] # every 2nd element
let slice = tensor[_.._|2] # entire dim, every 2nd
let slice = tensor[|2] # NEW: cleaner syntax for every 2nd (Python [::2])
let slice = tensor[|2, 3] # every 2nd of dim 0, index 3 of dim 1
# Negative steps (reversing) - NOT supported, use flip()
let reversed = tensor.flip(@[0]) # Reverse along dimension 0
# Span slices (whole dimension, equivalent to Python ":")
let span = tensor[_, 3] # all of dim 0, index 3 of dim 1
let span = tensor[1.._, _] # dim 0 from 1, all of dim 1
let span = tensor[_.._] # entire dimension (Slice)
# Ellipsis for multi-dimensional expansion (Python "...")
let result = tensor[IndexEllipsis, 0] # last dimension index 0
let result = tensor[0, IndexEllipsis] # first dimension index 0, rest all
let result = tensor[1, IndexEllipsis, 0] # first dim index 1, last dim index 0
# Combined
let slice = tensor[-2..-1, 0..<5] # last 2 elements, first 5 of dim 1
| Nim syntax | Python equivalent | libtorch equivalent | Notes |
|---|---|---|---|
_ | : | Slice() | Full span of one dimension |
_.._ | : | Slice() | Full span (not Ellipsis!) |
| ` | step` | ::step | Slice(None, None, step) |
... | ... | Ellipsis | Expands to fill remaining dims |
-1 | -1 | N/A | Last element (exclusive in slicing) |
-2 | -2 | N/A | Second-to-last element |
Python slices are EXCLUSIVE on the end. Nim uses .. (inclusive) or ..< (exclusive), but for negative indices we use ..- following Python semantics directly.
| Python syntax | Nim syntax | Result indices | Description |
|---|---|---|---|
t[:] | t[_.._] | 0,1,2,3,4 | All elements |
t[:2] | t[_..<2] | 0,1 | First 2 elements |
t[2:] | t[2.._] | 2,3,4 | From index 2 to end |
t[1:4] | t[1..<4] | 1,2,3 | Elements 1,2,3 |
t[::2] | `t[.. | 2]ort[ | 2]` |
t[1::2] | `t[1.._ | 2]` | 1,3 |
t[:4:2] | `t[_..<4 | 2]` | 0,2 |
t[1:4:2] | `t[1..<4 | 2]` | 1,3 |
t[:-1] | t[_..-1] | 0,1,2,3 | All but last |
t[-3:] | t[-3.._] | 2,3,4 | Last 3 elements |
t[-3:-1] | t[-3..-1] | 2,3 | 3rd-from-end to before last |
t[::-1] | t.flip([dim]) | - | Reverse (use flip()) |
For a 5-element array (indices 0, 1, 2, 3, 4):
| Python | Nim | Actual indices | Explanation |
|---|---|---|---|
| -1 | -1 | 4 | Last element |
| -2 | -2 | 3 | Second-to-last |
| -3 | -3 | 2 | Third-to-last |
a[:-1] | a[_..-1] | 0,1,2,3 | Stop at -1 (4), exclusive → 0-3 |
a[-3:] | a[-3.._] | 2,3,4 | Start at -3 (2), go to end |
a[-3:-1] | a[-3..-1] | 2,3 | Start at -3 (2), stop before -1 (4) |
Key insight: In Python slicing, -1 as stop means "up to but NOT including the last element". The libtorch Slice constructor follows Python's exclusive upper bound semantics.
...) vs Span (_)The _ symbol and ... ellipsis have different meanings:
_ (Span): Selects the entire dimension. Equivalent to Python's : or libtorch's Slice().
tensor[_, 3] = tensor[:, 3] = all of dim 0, specific index of dim 1tensor[_.._] = tensor[:, :] = all dimensions fully selected... (Ellipsis): Expands to fill remaining dimensions. Equivalent to Python's ... and libtorch's torch::indexing::Ellipsis.
tensor[..., 0] = tensor[:, :, :, 0] = ellipsis expands to match tensor ranktensor[0, ...] = tensor[0, :, :, :] = leading index, ellipsis fills resttensor[1, ..., 0] = tensor[1, :, :, 0] = indices at start and end, ellipsis in middleInternal workaround for operator precedence:
type Step = object
b: int # end of range
step: int # step size
# Operators:
# `|` - step operator (positive only)
# `..` - inclusive range
# `..<` - exclusive range
# `..-` - end-relative range (Python exclusive semantics)
# `|-` - negative step (NOT SUPPORTED - use flip() instead)
Note: Negative steps (|-) are not supported because libtorch's Slice() doesn't support them.
To reverse a dimension, use tensor.flip(@[dim]) instead.
Negative indices are normalized at runtime using pythonSliceToTorchSlice function:
func pythonSliceToTorchSlice*(
start: int | Nullopt_t,
stop: int | Nullopt_t,
size: int
): TorchSlice {.inline.} =
## Convert Python-style slice to libtorch Slice with proper negative index handling.
##
## Python semantics:
## - Exclusive upper bound (stop is not included)
## - Negative indices are normalized: -N → size + (-N) = size - N
##
## Example for size=5, stop=-1:
## -1 + 5 = 4 → Slice(start, 4) which gives elements [start, 4)
## Since stop=4 is exclusive, we get elements up to index 3 (all but last)
This function is automatically called during tensor indexing when negative indices are detected.
Types for dispatching different indexing modes:
type FancySelectorKind* = enum
FancyNone # No fancy indexing
FancyIndex # Integer array indexing
FancyMaskFull # Boolean mask on full tensor
FancyMaskAxis # Boolean mask on specific axis
FancyUnknownFull # Unknown selector full
FancyUnknownAxis # Unknown selector axis
# Desugar all syntactic sugar to TorchSlice
desugarSlices*(args: untyped)
# Typed dispatch for read operations
slice_typed_dispatch*(t: typed, args: varargs[typed])
# Typed dispatch for write operations
slice_typed_dispatch_mut*(t: typed, args: varargs[typed], val: typed)
Test patterns for negative indexing:
suite "Python Slice Syntax to Nim Translation Reference":
## For a 5x5 Vandermonde matrix (indices 0-4 on each axis):
## [[1,1,1,1,1], [2,4,8,16,32], [3,9,27,81,243], [4,16,64,256,1024], [5,25,125,625,3125]]
test formatName("Python a[:-1] -> Nim a[_..-1]", "a[:-1]"):
## Nim: a[_..-1] gets all but last (stop=-1 is exclusive)
## Python: a[:-1] gets all but last element
let t = genShiftedVandermonde5x5(kFloat64)
let sliced = t[_..-1, _]
check: sliced.shape[0] == 4 # indices 0,1,2,3 (not 4)
check: sliced[3, 0].item(float64) == 4.0 # Row 3, not row 4
test formatName("Python a[-3:] -> Nim a[-3.._]", "a[-3:]"):
## Nim: a[-3.._] gets last 3 (start=-3 means 3rd from end)
## Python: a[-3:] gets last 3 indices (2,3,4)
let t = genShiftedVandermonde5x5(kFloat64)
let sliced = t[-3.._, _]
check: sliced.shape[0] == 3 # indices 2,3,4
check: sliced[0, 0].item(float64) == 3.0 # Row 2
check: sliced[2, 0].item(float64) == 5.0 # Row 4
test formatName("Python a[-3:-1] -> Nim a[-3..-1]", "a[-3:-1]"):
## Nim: a[-3..-1] gets 3rd-from-end to before last
## Python: a[-3:-1] gets indices 2, 3 (exclusive stop)
let t = genShiftedVandermonde5x5(kFloat64)
let sliced = t[-3..-1, _]
check: sliced.shape[0] == 2 # indices 2, 3
check: sliced[0, 0].item(float64) == 3.0 # Row 2
check: sliced[1, 0].item(float64) == 4.0 # Row 3
Test FFT operations with roundtrip verification:
suite "FFT1D":
setup:
let shape = [8'i64]
let f64input = rand(shape.asTorchView(), kfloat64)
let c64input = rand(shape.asTorchView(), kComplexF64)
test "fft, ifft":
let fftout = fft(c64input)
let ifftout = ifft(fftout)
let max_input = max(abs(ifftout)).item(float64)
var rel_diff = abs(ifftout - c64input)
rel_diff /= max_input
check mean(rel_diff).item(float64) < 1e-12
test "rfft, irfft":
let fftout = rfft(f64input)
let ifftout = irfft(fftout)
# ... similar verification
suite "Tensor creation":
test "eye":
let t = eye(2, kInt64)
check t == [[1, 0], [0, 1]].toTorchTensor()
test "zeros":
let shape = [2'i64, 3]
let t = zeros(shape.asTorchView(), kFloat32)
check t == [[0.0'f32, 0.0, 0.0], [0.0'f32, 0.0, 0.0]].toTorchTensor()
test "linspace":
let steps = 120'i64
let reft = toSeq(0..<120).map(x => float64(x)/float64(steps-1)).toTorchTensor()
let t = linspace(0.0, 1.0, steps, kFloat64)
let rel_error = mean(t - reft)
check rel_error.item(float64) <= 1e-12
test "arange":
let steps = 130'i64
let step = 1.0/float64(steps)
let t = arange(0.0, 1.0, step, float64)
for i in 0..<130:
let val = t[i].item(float64)
let refval: float64 = i.float64 / 130.0
check (val - refval) < 1e-12
test "sort, argsort":
let t = [2, 3, 4, 1, 5, 6].toTorchTensor()
let
s = t.sort()
args = t.argsort()
check s.get(0) == [1, 2, 3, 4, 5, 6].toTorchTensor()
check s.get(1) == args
check args == [3, 0, 1, 2, 4, 5].toTorchTensor()
suite "Operator precedence":
test "+ and *":
let a = [[1, 2], [3, 4]].toTorchTensor()
let b = -a
check b * a + b == [[-2, -6], [-12, -20]].toTorchTensor()
check b * (a + b) == [[0, 0], [0, 0]].toTorchTensor()
test "+ and .abs":
let a = [[1, 2], [3, 4]].toTorchTensor()
let b = -a
check a + b.abs == [[2, 4], [6, 8]].toTorchTensor()
check (a + b).abs == [[0, 0], [0, 0]].toTorchTensor()
proc runTests*() to avoid C++ = {} initializationshape.asTorchView() for shape parametersdtype.toTorchType() for dtype conversion== for tensor comparison (uses equal() internally)case must assign to resultabs() and mean() of differencemax(abs(tensor)).item(float64) for normalization in error calculations