--- title: "Checkpoint and resume long fits" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Checkpoint and resume long fits} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") set.seed(20260716) ``` ```{r cran-gate, include = FALSE} # The model fits below are evaluated when the article is built locally, in CI # and for the pkgdown site (all of which set NOT_CRAN). CRAN's check farm gives # the whole check a ten-minute budget, which these fits do not fit inside, so # there the code is shown without being run. EVAL_FITS <- identical(Sys.getenv("NOT_CRAN"), "true") knitr::opts_chunk$set(eval = EVAL_FITS) ``` ## Installation ```{r, eval = FALSE} # Current release, built for Windows and macOS install.packages("tulpa", repos = c("https://gcol33.r-universe.dev", "https://cloud.r-project.org")) # Development version from GitHub install.packages("pak") pak::pak("gcol33/tulpa") # Pin a release pak::pak("gcol33/tulpa@v0.6.0") ``` `pak` resolves the dependency tree, including `tulpaMesh` (on CRAN, used for SPDE mesh construction). `tulpa` compiles its C++ backend on first install, so a C++17 toolchain is required: Rtools on Windows, Xcode CLI tools on macOS, `r-base-dev` on Linux. ```{r load, message = FALSE} library(tulpa) ``` ## Why checkpoint A nested-Laplace fit is a loop over independent hyperparameter grid cells; a multi-chain NUTS fit is a loop over independent chains. Both can run for a long time, and a killed or rebooted run should not have to start over. tulpa writes each finished unit to a content-addressed append log: on resume it reloads the completed units and runs only the rest, and a run interrupted mid-write is detected and re-run rather than trusted. ## Turn it on Every nested-Laplace fitter takes `control$checkpoint = list(path =, resume =)`. `path` is the checkpoint file; `resume = TRUE` reloads any completed cells from it, `resume = FALSE` (the default) starts fresh and overwrites. ```{r fit} # A small nested-Laplace fit over an ICAR field on a chain graph. S <- 30L W <- matrix(0, S, S) for (i in 1:(S - 1)) W[i, i + 1] <- W[i + 1, i] <- 1 df <- data.frame(region = factor(seq_len(S))) df$x <- as.integer(df$region) / 10 + rnorm(S, 0, 0.3) df$y <- rbinom(S, 20, plogis(-0.4 + 0.5 * df$x)) ckpt <- tempfile(fileext = ".ckpt") fit <- tulpa(y ~ x + spatial(region), data = df, family = "binomial", n_trials = rep(20L, S), spatial = spatial_car(W, level = "obs"), mode = "structured", control = list(checkpoint = list(path = ckpt, resume = FALSE))) coef(fit) ``` The grid cells are now on disk: ```{r size} file.exists(ckpt) && file.info(ckpt)$size > 0 ``` ## Resume A second call with the same data, settings, and grid plus `resume = TRUE` reloads every completed cell instead of re-solving it -- so this call returns essentially instantly and to the same result. ```{r resume} fit2 <- tulpa(y ~ x + spatial(region), data = df, family = "binomial", n_trials = rep(20L, S), spatial = spatial_car(W, level = "obs"), mode = "structured", control = list(checkpoint = list(path = ckpt, resume = TRUE))) all.equal(coef(fit), coef(fit2)) ``` ```{r cleanup, include = FALSE} unlink(ckpt) ``` ## What the fingerprint protects The checkpoint header carries a fingerprint of the data, settings, and grid. Resuming onto a file written for a *different* fit errors rather than silently mixing results, and a torn final record (a run killed mid-write) is truncated and re-run. A multi-chain NUTS resume is bit-for-bit identical to the uninterrupted run, because a chain is deterministic in its seed and data.