---
title: "Parallel and threaded fitting (including HPC)"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Parallel and threaded fitting (including HPC)}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>", eval = FALSE)
library(flexstanr)
```

Fitting is the expensive part of most Stan analyses. flexstanr can put your
machine's spare cores to work for you from a single switch:

```{r}
stan_options(threading = TRUE)
```

That is the whole user story. This article explains what it does, how to shape
it (a core cap, HPC allocation), and the one thing a *model author* has to add
so the switch behaves well on a model that cannot thread.

## What `threading = TRUE` does

A fit can use cores two ways at once:

1. **Across chains.** MCMC chains are independent, so they run in parallel with
   near-linear speedup and no coordination overhead.
2. **Within a chain (threading).** If your Stan model sums a
   conditionally-independent term with `reduce_sum()` (or `map_rect`), each chain
   can split that sum across threads.

`stan_options(threading = TRUE)` looks at the cores the process is allowed to
use, **fills chain-parallelism first** (it is "free" -- no threading overhead),
gives the leftover cores to threads, writes the result onto the options, and
**tells you what it chose**:

```{r}
opts <- stan_options(chains = 4, threading = TRUE)
#> flexstanr: threading enabled. Using 16 of 16 available cores: 4 chains in
#> parallel, 4 threads per chain. Pass max_cores to leave some cores free.
```

The split, given a pool of cores to divide (the pool is all your machine's
cores, or your `max_cores`):

| chains | cores to split | chains in parallel | threads per chain |
|-------:|---------------:|-------------------:|------------------:|
| 4      | 4              | 4                  | 1 (no spare)      |
| 4      | 8              | 4                  | 2                 |
| 4      | 16             | 4                  | 4                 |
| 2      | 8              | 2                  | 4                 |

So threading only "switches on" when the pool has more cores than chains --
otherwise every core is spent running chains. Because the default uses all your
cores, an 8-core machine running 4 chains splits a pool of 8 (4 chains, 2
threads each).

You then fit as usual; `fit_model()` applies the allocation to whichever backend
you are on (and, for cmdstanr, compiles the model with threading enabled):

```{r}
fit <- fit_model("my_model", dat_stan = dat, init = init, stan_opts = opts)
```

## Capping the cores

By default `threading = TRUE` uses **all available cores**. If your machine
becomes unresponsive during a fit, cap it with `max_cores` (e.g.
`max_cores = 4`):

```{r}
# use at most 4 cores, however many the machine has
stan_options(chains = 4, threading = TRUE, max_cores = 4)
```

`threading = FALSE` (the default) leaves parallelism untouched, so you can still
set `cores` (rstan) or `parallel_chains` / `threads_per_chain` (cmdstanr) by hand
if you want full manual control.

## Model authors: warn when threads can't be used

Threading only helps if the model actually uses `reduce_sum()`/`map_rect` and was
compiled for threading. flexstanr cannot know that about *your* model -- so if
you write a fit function on top of flexstanr, check whether the caller asked for
threads and warn when your model can't use them. `test_threaded()` reports
what the options request:

```{r}
run_my_fit <- function(dat_stan, init, stan_opts = stan_options()) {
  if (test_threaded(stan_opts) && !model_is_threaded) {
    warning(
      "threads were requested but this model does not use reduce_sum(); ",
      "it will run one thread per chain. Run more chains, use the threaded ",
      "model variant, or set threading = FALSE.",
      call. = FALSE
    )
  }
  fit_model("my_model", dat_stan = dat_stan, init = init, stan_opts = stan_opts)
}
```

This keeps the division of labor clean: the *user* just says `threading = TRUE`,
and the *model author* -- the only one who knows whether the model can thread --
tips them off when it won't take effect.

## How flexstanr decides how many cores to use

It asks [`parallelly::availableCores()`](https://parallelly.futureverse.org/) for
the number of cores the **process is allowed** to use -- crucially **not**
`parallel::detectCores()`. It respects

* the HPC scheduler's allocation (`SLURM_CPUS_PER_TASK`, PBS, SGE, LSF),
* cgroup CPU quotas (containers, cpuset),
* `options(mc.cores = ...)`,

and returns 2 under `R CMD check`. This makes `threading = TRUE` safe to use even
on a shared cluster.

### Compiling your model for threading

Within-chain threads only do anything if the model was **compiled with
threading**:

* **cmdstanr** -- `fit_model()` compiles with `cpp_options = list(stan_threads =
  TRUE)` automatically when the allocation asks for more than one thread, into a
  separate cache directory so a threaded and a non-threaded build coexist.
  Nothing to do.
* **rstan** -- the host package's models must have been built with threading
  support (a package-build concern); flexstanr only sets `STAN_NUM_THREADS`
  around the fit. If a model was not built for threading, the setting is inert --
  which is exactly what `test_threaded()` lets you warn about.

## On a laptop or workstation

Nothing to do: `threading = TRUE` detects your cores and splits them, using all
of them. To leave some free, use `max_cores`, or cap globally with `mc.cores`:

```{r}
options(mc.cores = 6)
stan_options(chains = 4, threading = TRUE)   # sees at most 6 cores
```

## On an HPC cluster

### The trap to avoid

`parallel::detectCores()` reports **every** core on the compute node (often 64,
128+) regardless of your job's allocation. A fit sizing itself from that would
launch dozens of threads inside a job pinned to a handful of cores; the
scheduler's cgroup would throttle it to a crawl. Because `threading = TRUE` uses
`parallelly::availableCores()`, it reads your *allocation* instead.

### Single node: let flexstanr split your allocation

Request one task with several CPUs and let `threading = TRUE` divide them. For
4 chains x 2 threads (8 cores):

```bash
#!/bin/bash
#SBATCH --job-name=stan-fit
#SBATCH --nodes=1
#SBATCH --ntasks=1
#SBATCH --cpus-per-task=8      # availableCores() will report 8
#SBATCH --mem=16G
#SBATCH --time=04:00:00
Rscript fit.R
```

```{r}
# fit.R -- no core arithmetic; availableCores() picks up SLURM_CPUS_PER_TASK = 8
opts <- stan_options(chains = 4, iter = 2000, threading = TRUE)
fit  <- fit_model("my_model", dat_stan = dat, init = init, stan_opts = opts)
saveRDS(fit, "fit.rds")
```

### Many chains across nodes: one chain per array task

Chain-parallelism and threads are **shared-memory**: they cannot span nodes. To
scale beyond a single node, run **one chain per job** with a SLURM array, give
each job all its cores for threading, and combine the single-chain fits after.

```bash
#SBATCH --array=1-4            # one task per chain
#SBATCH --nodes=1
#SBATCH --cpus-per-task=8      # all 8 cores -> threads for this one chain
```

```{r}
i    <- Sys.getenv("SLURM_ARRAY_TASK_ID")
opts <- stan_options(chains = 1, seed = as.integer(i), threading = TRUE)
fit  <- fit_model("my_model", dat_stan = dat, init = init, stan_opts = opts)
saveRDS(fit, sprintf("chain-%s.rds", i))
```

`stan_options()` is otherwise for how the sampler explores (`iter`, `warmup`,
`seed`, `control = list(adapt_delta = ...)`, ...); `threading` is the one knob
that reaches out to the machine.
