The backend works on the caller’s data wherever possible. An R matrix is bound by its memory address, a memory-mapped file by its mapping, and the preprocessing a method needs is applied in place and undone afterwards. A design of several gigabytes is therefore held once, by R or by the file, and not a second time by the backend. This page states the contract that makes it safe.
R stores a matrix of whole numbers as integers. The backend binds double storage, so an integer matrix, a data frame or a logical matrix is converted first, and the conversion is a copy by R’s own rules. The conversion happens visibly in R before the call, once, and a double matrix passes through untouched. To keep a large design copy-free, create it as double.
A selector centers and scales X in place when it is
constructed and writes the original values back when
select() returns. In between, X holds the
working design. restore() writes back early for a selector
that will not run, and a selector that is garbage collected without
running writes back in its finalizer.
set.seed(1)
n <- 80
p <- 40
X <- matrix(rnorm(n * p), n, p)
y <- as.numeric(X[, 1] * 2 + rnorm(n))
X_before <- X + 0
sel <- TRexSelector$new(X, y, tFDR = 0.1, seed = 2, verbose = FALSE, control = trex_control(K = 10))
round(sqrt(colSums(X^2))[1:3], 6) # the working design: unit columns
#> [1] 1 1 1
sel$select()
max(abs(X - X_before)) # the original values are back
#> [1] 4.440892e-16A T-solver with normalize = TRUE or
intercept = TRUE is a writer. It transforms X
and D in place, keeps the statistics, and holds a claim on
both matrices until restore() or garbage collection writes
them back. A second writer on a claimed matrix is refused, because a
second transformation would move the data by rounding.
D <- matrix(rnorm(n * p), n, p)
w1 <- TLARS_Solver$new(X, D, y)
tryCatch(TLASSO_Solver$new(X, D, y), error = function(e) cat("error:", conditionMessage(e), "\n"))
#> error: TLASSO: the input matrix X is already held by another live T-solver or T-Rex selector that preprocessed it in place. A second in-place preprocessing is not idempotent (it moves the data by rounding) and would corrupt both. Pre-scale once in front and construct the solvers with normalize = false and intercept = false (readers never claim a buffer), or restore() the previous solver, or let it go out of scope first.
w1$restore()A solver with normalize = FALSE and
intercept = FALSE is a reader. It runs on the data as it
finds them and never writes, so any number of readers can share one
design. A pipeline scales the design once with a scaler and constructs
readers.
sx <- MLLpNormScaler$new(norm_type = 2, center = TRUE)
invisible(sx$fit_transform_inplace(X))
sd <- MLLpNormScaler$new(norm_type = 2, center = TRUE)
invisible(sd$fit_transform_inplace(D))
y_c <- y - mean(y)
opts <- tsolver_options(normalize = FALSE, intercept = FALSE, precentered = TRUE)
r1 <- TLARS_Solver$new(X, D, y_c, options = opts)
r2 <- TLASSO_Solver$new(X, D, y_c, options = opts)
r1$execute_step(T_stop = 2)
r2$execute_step(T_stop = 2)
invisible(sx$inverse_transform_inplace(X))
invisible(sd$inverse_transform_inplace(D))
max(abs(X - X_before))
#> [1] 8.881784e-16y is never written. The solver centers an internal copy
of the response.
load() restores a solver into one constructed on the
same data in the same state. Load into a reader on pre-scaled data, or
into a writer constructed with the same options on the same raw data,
which then keeps the duty to write back.
A memory-mapped matrix is bound through the same interface as an R
matrix. The backend touches the pages it needs and the file keeps the
data, so the working set of a run is the backend’s own buffers, never a
copy of the design. The R-level accessors ([ and
as.matrix()) do copy the requested block into R memory, by
design.
The ridge and elastic-net classes have a low_memory
engine that avoids every allocation of the size of X.
| Class | low_memory = FALSE |
low_memory = TRUE |
|---|---|---|
MLRidgeCV |
each fold gathers and standardizes its training rows and takes a
thin SVD, about three to four copies of X in flight |
the dual (kernel) form, \(n \times n\) kernels accumulated from column blocks read in place, memory \(O(n^2)\) |
MLElasticNet |
coordinate descent on a centered and standardized working copy, one \(n \times p\) matrix | coordinate descent on X itself, no allocation of the
size of X |
MLElasticNetCV |
one working copy per fit | every fold on X itself with a training mask |
The two engines compute the same estimator on the same grid and agree
to rounding. The selectors that tune \(\lambda_2\) by cross-validation (GVS, SPCA,
Tikhonov) use these engines, and the elastic-net tuner of the sparse PCA
selector keeps its peak memory under 0.3 times the size of
X.
get_memory_bytes() reports what a T-solver holds on the
heap. X and D are borrowed and not
counted.