---
title: "Compute Backends: CPU, GPU and Cloud"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Compute Backends: CPU, GPU and Cloud}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
```

```{r setup, message = FALSE}
library(tidylearn)
```

## Where a fit runs

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:

- **Can this method go faster on a GPU?** Only two can.
- **Will this job fit on this machine?** Any of them might not.

## What this machine can do

```{r check-gpu, eval = FALSE}
tl_check_gpu()
```

`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.

## Only two methods have a GPU path

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:

```{r gpu-routing, eval = FALSE}
# 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.

## Estimating before you commit

`tl_compute_advisor()` answers that. It is arithmetic over the problem
dimensions, so it needs neither a GPU nor the backend package installed:

```{r advisor}
advice <- tl_compute_advisor(
  "xgboost", iris, Species ~ .,
  hyperparams = list(nrounds = 1000)
)

advice$recommendation
```

```{r advisor-print}
advice
```

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 compute

**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.

### Consent is explicit

Nothing is uploaded without you saying so, either per call or for the
session:

```{r consent}
# For the session
tl_cloud_consent()

# Revoke early
tl_cloud_consent(FALSE)
```

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.

### Destinations are checked

The endpoint is read from an environment variable:

```{r endpoint, eval = FALSE}
Sys.setenv(
  TIDYLEARN_MODAL_ENDPOINT = "https://you--tidylearn-fit.modal.run"
)
```

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:

```{r hosts}
tl_cloud_allowed_hosts()
```

Modal customers serving Web Functions from a custom domain can add it,
for the session:

```{r allow-host}
tl_cloud_allow_host("fits.example.com")
tl_cloud_allowed_hosts()
```

```{r allow-host-reset, include = FALSE}
tl_cloud_allow_host(NULL)
```

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.

### Bounding the cost

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:

```{r cost, eval = FALSE}
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:

```{r jobs}
tl_cloud_jobs()
```

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.

### The contract is written down

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:

```{r threat-model, eval = FALSE}
file.show(
  system.file("security/threat-model.md", package = "tidylearn")
)
```

It covers token handling, egress consent, ephemeral compute, the absence
of telemetry, and destination validation.

## What is coming

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.

## See also

- `vignette("getting-started", package = "tidylearn")`
- `vignette("supervised-learning", package = "tidylearn")`
- `?tl_check_gpu`, `?tl_compute_advisor`, `?tl_cloud_consent`
