Most of the time, nowhere interesting: tl_model() runs
on the CPU and you never think about it. This vignette is about the
cases where you should — a fit that would take hours, or one that will
not fit in memory at all.
tidylearn separates two questions that are easy to conflate:
tl_check_gpu() parses nvidia-smi and checks
which GPU-capable backends are installed. It is cheap by design: it
reads which packages are installed from the library without loading
them, so it does not start Python, import TensorFlow, or fit anything,
and it gives nvidia-smi 10 seconds to answer before warning
and reporting no GPU. It returns an object with a print()
method describing the CUDA driver, the devices found, and which of
xgboost, keras3 and tensorflow are installed.
Of the thirteen supervised methods, only "xgboost" and
"deep" have an upstream GPU implementation. The other
eleven — linear, polynomial and logistic regression, ridge, LASSO,
elastic net, decision trees, random forests, gradient boosting, SVM and
neural networks — wrap packages that have no CUDA path at all. Asking
for a GPU gets you a warning and a CPU fit:
# Routed to CUDA when a capable backend is present
model <- tl_model(data, y ~ ., method = "xgboost", compute = "gpu")
# Warns and falls back to CPU: randomForest has no GPU implementation
model <- tl_model(data, y ~ ., method = "forest", compute = "gpu")
# Let tidylearn decide per call
model <- tl_model(data, y ~ ., method = "xgboost", compute = "auto")For those eleven methods the question to ask is whether the job fits in memory. Their fits ran on one core when timed for the advisor, so extra cores do not speed them up.
tl_compute_advisor() answers that. It is arithmetic over
the problem dimensions, so it needs neither a GPU nor the backend
package installed:
advice <- tl_compute_advisor(
"xgboost", iris, Species ~ .,
hyperparams = list(nrounds = 1000)
)
advice$recommendation
#> [1] "cpu"advice
#> <tidylearn compute advice>
#> Problem: xgboost on 150 rows x 4 cols (~0.0 MB)
#>
#> Tier estimates (order-of-magnitude):
#> Local CPU: 0.0s (peak RAM ~0 MB)
#> Local GPU: -- [not applicable]
#> Cloud: 45.0s (~$0.04) [A10G (24 GB VRAM / 24 GB RAM)] [not configured]
#>
#> Recommendation: cpu
#>
#> Reasoning:
#> - Estimated local CPU runtime ~0.0s. Cloud cold-start (~45s) would dominate; just run it locally.
#>
#> Notes:
#> - Method 'xgboost' could use GPU, but no GPU-capable backend was detected. See ?tl_check_gpu.
#> - Cloud integration is not yet configured in tidylearn. Estimates shown so users can see the tier's shape; actual submission is not yet supported.The advisor covers all thirteen supervised methods, and treats cloud
as a memory headroom tier. It will recommend cloud for a
CPU-only method such as random forest or SVM when the job is
RAM-infeasible locally, which is the common case for large data. On a
small dataset like iris it will tell you to stay local.
Estimates are order-of-magnitude. Each method’s runtime constant comes from timing fits of three sizes on one laptop, where the estimates land within about a factor of 3 of those fits. Two runs on that laptop differed by up to 2.5x, and other hardware or data shapes can be further off. The memory multiples and GPU speedups are assumptions. Modal’s prices were read from https://modal.com/pricing on 2026-10-30. Treat the output as a decision aid.
Unsupervised methods are not modelled. Calling the advisor on one errors.
Cloud execution is not available yet.
compute = "cloud" errors with a message saying so, and the
advisor’s cloud tier is reported for planning only. The consent gate,
the host allowlist and the cost calculations below are in the package
today, ahead of the submission path that will use them. The job timeout,
the worker’s retry setting and cancelling a job are the design that path
will follow, and have no code yet.
A cloud fit will upload your training data to your own Modal account. That is a third party, and it may be something your organisation’s data-handling rules forbid. tidylearn treats that transfer as the consequential step it is.
Nothing is uploaded without you saying so, either per call or for the session:
# For the session
tl_cloud_consent()
#> Cloud uploads enabled for this R session. Training data passed to tl_model(compute = 'cloud') will be sent to your Modal account. Revoke with tl_cloud_consent(FALSE).
# Revoke early
tl_cloud_consent(FALSE)
#> Cloud uploads disabled for this R session.The session lock is held in memory only. It is never written to disk
and does not survive an R restart. There is also no interactive prompt
anywhere in the cloud path, so Rscript, CI and knitted
documents behave exactly like an interactive session. A prompt that
cannot be answered in batch would block them.
Per call, the equivalent will be confirm_upload = TRUE,
which tl_model() does not take yet.
The endpoint is read from an environment variable:
An environment variable rather than an R option, deliberately: a
shared .Rprofile can set an option without you noticing,
and the whole point of this check is that a destination should not
appear silently.
Before anything is sent, the URL must parse, use https,
and resolve to a host on the allowlist. Anything else errors. The
default allowlist is Modal’s own domains:
Modal customers serving Web Functions from a custom domain can add it, for the session:
tl_cloud_allow_host("fits.example.com")
#> Cloud uploads may now also go to: fits.example.com. This is in addition to Modal's own hosts and lasts for this R session only.
tl_cloud_allowed_hosts()
#> [1] "modal.run" "modal.com" "fits.example.com"Matching is anchored, so allowing a host allows exactly it and its
subdomains. fits.example.com does not admit
example.com, evil-fits.example.com, or
fits.example.com.evil.test. Single-label names such as
"com" are refused outright, since they would open an entire
top-level domain.
A job submitted to Modal runs there to completion whatever your R session does next. Pressing Ctrl-C, closing the IDE, a crashed session and a closed laptop all leave it running, because the session was only polling for a result. A job you thought you had abandoned keeps billing until it finishes or something kills it.
No amount of care in the R client can guarantee anything against that, because a killed session runs no cleanup handlers. So the controls are layered by what survives:
What will hold even if your session dies. Each submission will set an explicit timeout, derived from the estimate with headroom and capped well below Modal’s 24-hour maximum, so Modal’s default is never inherited. The worker will run with retries disabled, because Modal applies timeouts per attempt and three retries would bill four full timeouts for one hung job. You should also set a spend budget on your Modal workspace, which is the only true hard cap and is not tidylearn’s to set.
What will be best-effort. The pre-flight gates, the job registry and cancel-on-interrupt will catch the ordinary cases, and none of them survive a killed session.
Before a fit, the figure you will be asked to accept is the worst case rather than the estimate: the timeout plus the load and idle time Modal also bills, at the tier’s rate. This is the planned call, which errors today:
model <- tl_model(data, y ~ ., method = "xgboost", compute = "cloud",
confirm_upload = TRUE, max_cost = 5)That will refuse before anything is uploaded if the worst case
exceeds max_cost. It will also refuse a fit whose estimate
is so large that the job would be killed by its own timeout before
finishing — submitting that would bill the full timeout and return
nothing.
tl_cloud_jobs() lists the jobs in flight, which is none
until submission exists:
tl_cloud_jobs()
#> # A tibble: 0 × 5
#> # ℹ 5 variables: call_id <chr>, method <chr>, submitted_at <dttm>,
#> # timeout_seconds <int>, worst_case_cost <dbl>A job will leave that list when its result is collected or it is cancelled. The list itself does not outlive the session, and a job’s timeout does.
What cloud compute will and will not do is documented as a threat model that ships with the package, including an audit checklist a reviewer can run against the source:
It covers token handling, egress consent, ephemeral compute, the absence of telemetry, and destination validation.
The remaining work is the submission path itself — building the
authenticated request, uploading, polling for the result, and restoring
the fitted model in your session. Models cross that boundary as bytes;
twelve of the thirteen methods serialise natively, and
"deep" carries its keras model as a separate
.keras archive because a keras model is a reference to a
Python object rather than an R one.
Until that lands, use tl_compute_advisor() to size the
problem and run locally.
vignette("getting-started", package = "tidylearn")vignette("supervised-learning", package = "tidylearn")?tl_check_gpu, ?tl_compute_advisor,
?tl_cloud_consent