The Backend Facade¶
tinyTT runs on PyTorch, and on nothing else since 0.5. Everything in the package reaches it through one module:
tinytt.backend is the public name of the package-internal tinytt._backend:
the same module object, not a copy, so a default device or dtype set through
one is seen through the other. Code inside the package imports _backend;
user code should import backend. tinytt.functional and
tinytt.linesearch are public names for _functional and _linesearch in
the same way.
Why a facade at all, with one backend behind it¶
The facade is where device, dtype and complex-promotion policy live. Reaching
around it is how the 0.4 tree ended up with a _mode_ops helper that forced
float64 on CPU regardless of the input, and with a TT.norm() that
materialised the full tensor. One import site for tensor creation means one
place to fix that kind of thing.
ruff will not catch a stray import torch; a grep will:
TINYTT_BACKEND¶
Unset it. It is still read, but only so that a stale setting fails loudly rather than silently:
| Value | Behaviour |
|---|---|
unset, or pytorch |
normal |
tinygrad or tiny |
ValueError naming tinygrad — the backend was removed in 0.5, warned for one release, and refuses in 0.6 |
| anything else | ValueError |
0.5 warned and loaded PyTorch anyway. That was the wrong default for a variable whose only job is to choose a backend: the caller asked for tinygrad and got something else without noticing.
Devices¶
PyTorch handles the device; pass device= to any constructor.
# docs-check: skip needs a CUDA device; the CPU spelling is shown below
x = tt.random([4] * 6, 3, device="cuda", dtype=tn.float64)
y = x.to("cpu")
On the CPU the same call is:
x = tt.random([4] * 6, 3, device="cpu", dtype=tn.float64)
print(x.cores[0].device, x.cores[0].dtype)
Not every accelerator supports float64 — Metal does not. The backend exposes a probe, and the device-parametrised tests use it to fall back to float32 with a looser tolerance:
Set TINYTT_DEVICE to run those tests somewhere other than the CPU:
They run on the CPU when it is unset. (Until 0.6 they skipped when it was
unset, which meant fast_matvec, dmrg_matvec and the TTM-multiply autograd
path had no coverage in CI at all.)
Complex support¶
tn.complex64 and tn.complex128 are available, with tn.is_complex_dtype
and tn.real_dtype for dispatching. Complex is supported in the tangent and
dynamics layers and in solvers.eig_solve, whose interface tensors conjugate
the bra side. solvers.amen_solve does not conjugate and is real-only;
the manifold preconditioners refuse complex input with a message saying why,
rather than silently building a non-Hermitian metric.
Autograd¶
Cores are ordinary torch.Tensor objects, so autograd reaches them directly.
tinytt.grad has watch / unwatch helpers, and tn.assign(core, value)
replaces the in-place assignment that the old backend spelled
core.assign(value).
from tinytt.grad import unwatch, watch
x = tt.random([4] * 3, 2)
watch(x)
loss = x.norm() ** 2
loss.backward()
for core in x.cores:
tn.assign(core, core.detach() - 0.1 * core.grad)
unwatch(x)
print("stepped, norm now", float(x.norm()))
Linear algebra¶
tn.linalg carries norm, qr, svd, eigh, cholesky, lstsq,
matrix_exp and solve. Two helpers avoid materialising a diagonal matrix,
which is worth it in the sweep inner loops:
s = tn.tensor(np.array([1.0, 2.0, 3.0]))
mat = tn.tensor(np.ones((3, 4)))
print(np.asarray(tn.to_numpy(tn.scale_rows(s, mat)))[:, 0]) # diag(s) @ mat
print(np.asarray(tn.to_numpy(tn.scale_cols(mat.transpose(0, 1), s)))[0])
tn.linalg.svd carries a guard for a platform bug: Apple's Accelerate gesdd
returns, for some rank-deficient inputs, a singular-value set that does not
carry the Frobenius energy of the input. Such a triple is not a decomposition
of the input and its vectors cannot be trusted either, so the backend checks
the energy, confirms on the residual, and redoes the decomposition with NumPy.
The check costs about 3% on a healthy CPU SVD.