greta#833 (wire-jit-compile): XLA compilation

Building a model, opt() and mcmc() on greta’s CRAN release, main and greta#833, all with model(compile = TRUE), which only #833 acts on. With XLA, opt() is slower, building a model and mcmc() take about as long, and the posteriors are the same.
Published

October 6, 2026

Setup
# the packages' start-up messages say nothing about the results
suppressPackageStartupMessages({
  library(dplyr)
  library(tidyr)
  library(ggplot2)
  library(bench)
  library(here)
})

post_path <- "posts/2026-10-06-jit-compile-i833"

# each `# ---- label ----` section of the scripts, shown below by its label
knitr::read_chunk(here(post_path, "benchmark.R"))
knitr::read_chunk(here(post_path, "run.R"))

# links to the scripts on GitHub, at the commit being rendered when GitHub
# Actions renders the site
github_ref <- Sys.getenv("GITHUB_SHA", "main")
github_file_url <- function(file) {
  paste0(
    "https://github.com/greta-dev/greta.benchmarks/blob/",
    github_ref,
    "/",
    post_path,
    "/",
    file
  )
}

# a link to the lines of a script's `# ---- label ----` section. Quarto escapes
# the markdown in an inline `{r}` result unless it is wrapped in I()
section_link <- function(file, label) {
  lines <- readLines(here(post_path, file))
  headings <- grep("^# ---- .+ ----$", lines)
  start <- headings[lines[headings] == paste("# ----", label, "----")]
  end <- c(headings, length(lines) + 1)[match(start, headings) + 1] - 1
  while (end > start && !nzchar(trimws(lines[end]))) {
    end <- end - 1
  }
  I(paste0(
    "[`", label, "`](", github_file_url(file), "#L", start, "-L", end, ")"
  ))
}

# "CRAN, main and #833"
paste_and <- function(x) {
  if (length(x) <= 1) {
    return(x)
  }
  paste(paste(x[-length(x)], collapse = ", "), "and", x[length(x)])
}

# links to several sections of a script, as "a, b and c"
section_links <- function(file, labels) {
  links <- vapply(
    labels,
    \(label) unclass(section_link(file, label)),
    character(1)
  )
  I(paste_and(unname(links)))
}

# white panels, so the grey band in the agreement figure shows
theme_set(theme_bw())

sessions <- list.files(
  here(post_path, "results"),
  pattern = "^session-[0-9]+[.]rds$",
  full.names = TRUE
) |>
  lapply(readRDS)

versions <- sessions[[1]]$versions
version_levels <- names(versions)
version_colours <- setNames(
  c("#E69F00", "#56B4E9", "#009E73")[seq_along(version_levels)],
  version_levels
)
bench_settings <- sessions[[1]]$results[[1]]$bench_settings
n_sessions <- length(sessions)

# one row per session and version, holding that version's results
runs <- lapply(sessions, function(session_file) {
  tibble(
    session = session_file$session,
    version = factor(names(session_file$results), levels = version_levels),
    position = match(names(session_file$results), session_file$order),
    results = unname(session_file$results)
  )
}) |>
  bind_rows()

model_levels <- names(runs$results[[1]]$build)
# each model's part of a section label in benchmark.R, such as
# bench-opt-multiple-linear
model_sections <- gsub("_", "-", model_levels)

# a part of the results that is a table or a value per run, stacked, with the
# run it came from
stack_tables <- function(part) {
  pieces <- lapply(seq_len(nrow(runs)), function(i) {
    values <- runs$results[[i]][[part]]
    if (!is.data.frame(values)) {
      values <- tibble(value = values)
    }
    tibble(session = runs$session[i], version = runs$version[i], values)
  })
  bind_rows(pieces)
}

# a part of the results that is a bench_mark per model, stacked into one
# bench_mark, with the run and model each came from
stack_marks <- function(part) {
  pieces <- list()
  for (i in seq_len(nrow(runs))) {
    marks <- runs$results[[i]][[part]]
    for (j in seq_along(marks)) {
      mark <- marks[[j]]
      mark$session <- runs$session[i]
      mark$version <- runs$version[i]
      mark$model <- names(marks)[j]
      pieces[[length(pieces) + 1]] <- mark
    }
  }
  vctrs::vec_rbind(!!!pieces)
}

provenance <- stack_tables("provenance")
sampler_iterations_per_draw <- stack_tables("sampler_iterations_per_draw")
build_marks <- stack_marks("build")
opt_marks <- stack_marks("opt")
mcmc_marks <- stack_marks("mcmc")

cran_version <- unique(provenance$greta_version[provenance$version == "CRAN"])

# one row per timed call: bench keeps every call's time in the `time` column
every_call <- function(marks) {
  marks |>
    as_tibble() |>
    select(session, version, model, time) |>
    mutate(call = lapply(time, seq_along)) |>
    unnest(c(time, call)) |>
    mutate(
      seconds = as.numeric(time),
      model = factor(model, levels = model_levels),
      .keep = "unused"
    )
}

# The first call on a model traces its TensorFlow functions, and on #833
# compiles them with XLA; later calls on the same model reuse them.
label_calls <- function(call) {
  last <- max(call)
  later <- if (last == 2) {
    "second call on the same model"
  } else {
    paste0("calls 2 to ", last, " on the same model")
  }
  factor(
    ifelse(call == 1, "first call on a new model", later),
    levels = c("first call on a new model", later)
  )
}

build <- every_call(build_marks)
opt_timings <- every_call(opt_marks) |>
  mutate(calls = label_calls(call))
mcmc_timings <- every_call(mcmc_marks) |>
  mutate(calls = label_calls(call))

# bench's own summary() of each mark, with the mean and longest of its calls
# added, all in seconds
bench_summary <- function(marks) {
  summaries <- summary(marks, filter_gc = FALSE)
  tibble(
    session = summaries$session,
    version = summaries$version,
    model = summaries$model,
    min = as.numeric(summaries$min),
    median = as.numeric(summaries$median),
    mean = vapply(marks$time, \(time) mean(as.numeric(time)), numeric(1)),
    max = vapply(marks$time, \(time) max(as.numeric(time)), numeric(1)),
    `itr/sec` = summaries$`itr/sec`,
    n_itr = summaries$n_itr,
    total_time = as.numeric(summaries$total_time)
  )
}

# each version's median call in each group, and how many times as long as
# #833's median it takes, so #833 is the unit
relative_medians <- function(timings, ...) {
  timings |>
    group_by(..., version) |>
    summarise(median_seconds = median(seconds), .groups = "drop_last") |>
    mutate(
      times_as_long_as_833 = median_seconds /
        median_seconds[version == "#833"]
    ) |>
    ungroup()
}

# three significant figures each, trailing zeros kept, where format() would pad
# a vector to the decimal places of its smallest value
format_number <- function(x) {
  formatC(signif(x, 3), format = "fg", digits = 3, flag = "#", big.mark = ",")
}

# how many times as long a version's median call takes as #833's
times_as_long <- function(timings, version = "main") {
  ratios <- relative_medians(timings)
  format_number(ratios$times_as_long_as_833[ratios$version == version])
}

version_scale <- scale_colour_manual(values = version_colours, guide = "none")
version_fill <- scale_fill_manual(values = version_colours, guide = "none")
jitter <- position_jitter(width = 0, height = 0.2, seed = 2026)

# A raincloud for each version: the density of its calls above, their box plot
# in the middle, and every call as a dot below. The summaries are drawn first
# and faint, so the dots sit on top.
raincloud <- list(
  ggdist::stat_halfeye(
    aes(fill = version),
    width = 0.4,
    justification = -0.3,
    .width = 0,
    point_colour = NA,
    alpha = 0.3
  ),
  geom_boxplot(
    aes(fill = version),
    width = 0.25,
    outlier.shape = NA,
    alpha = 0.3
  ),
  # a fixed dot size, so a call looks the same size in every panel
  ggdist::stat_dots(
    aes(fill = version),
    side = "bottom",
    layout = "swarm",
    justification = 1.3,
    scale = 0.75,
    binwidth = unit(2.6, "mm"),
    overflow = "compress",
    colour = NA,
    alpha = 0.6
  ),
  version_scale,
  version_fill,
  theme_bw(base_size = 13)
)
raincloud_caption <- "Each dot is one call, from every session. Above the dots, the curve is the density of the calls and the box marks their median and quartiles."
session_caption <- "Each small dot is one session's second mcmc() call. The large faint dot behind them is their mean."

# a searchable table, its numbers rounded for reading
dt_table <- function(data, digits = 3) {
  table <- DT::datatable(
    data,
    rownames = FALSE,
    filter = "top",
    options = list(pageLength = 10, autoWidth = TRUE)
  )
  decimal_columns <- names(data)[vapply(data, is.double, logical(1))]
  if (length(decimal_columns) == 0) {
    return(table)
  }
  DT::formatRound(table, decimal_columns, digits)
}

# a model's draws array is draws by chains by parameters
parameter_count <- function(name) {
  dim(runs$results[[1]]$mcmc_draws[[name]])[3]
}

# wide enough for a column of panels per parameter
draws_width <- function(name) {
  max(8, 1.6 * parameter_count(name))
}

greta#833 makes model(compile = TRUE) compile the model’s TensorFlow functions with XLA, and makes compile = FALSE the default. Every model here is built with compile = TRUE, on CRAN’s greta 0.6.0, on main and on #833; CRAN and main ignore the argument, so the difference between main and #833 is XLA. Building a model takes as long on all three, but opt() is slower with XLA: main takes 0.828 times as long as #833 on a model’s first call, and 0.624 times as long on later calls. mcmc() takes about as long, main taking 1.00 times as long as #833 on the first call and 0.995 times as long on the second, and all three sample the same posterior.

How long does building a model take?

Calling each model’s function, which creates its greta arrays and calls model(), 5 times in each session.

Code
build_summary <- bench_summary(build_marks)

ggplot(build, aes(x = seconds, y = version, colour = version)) +
  raincloud +
  facet_wrap(~model, ncol = 1, scales = "free_x") +
  labs(x = "Seconds", y = NULL)

Each dot is one call, from every session. Above the dots, the curve is the density of the calls and the box marks their median and quartiles.

Timed by benchmark.R’s bench-build-linear, bench-build-multiple-linear, bench-build-hierarchical-linear and bench-build-eight-schools sections.

Building a model compiles nothing, since XLA compiles on a model’s first call: over all models, main takes 1.01 times as long as #833, and CRAN 0.967 times as long.

Table: bench’s summary() of every mark
Code
dt_table(build_summary, digits = 4)

How long does opt() take?

opt(), 5 times on the same model: the first call traces the model’s TensorFlow functions, which #833 also compiles with XLA, and the later calls reuse them.

Code
opt_summary <- bench_summary(opt_marks)
opt_by_model <- relative_medians(opt_timings, model, calls)

# the models whose later calls are faster on #833 than on main
opt_later_faster <- opt_by_model |>
  filter(
    version == "main",
    calls != "first call on a new model",
    times_as_long_as_833 > 1
  ) |>
  pull(model) |>
  as.character()

ggplot(opt_timings, aes(x = seconds, y = version, colour = version)) +
  raincloud +
  facet_wrap(vars(model, calls), ncol = 2, scales = "free_x") +
  labs(x = "Seconds", y = NULL)

Each dot is one call, from every session. Above the dots, the curve is the density of the calls and the box marks their median and quartiles.

Timed by benchmark.R’s bench-opt-linear, bench-opt-multiple-linear, bench-opt-hierarchical-linear and bench-opt-eight-schools sections.

Over all models, main takes 0.828 times as long as #833 on a model’s first call, and 0.624 times as long on its later calls. #833’s later calls are faster than main’s only on hierarchical_linear and eight_schools.

Table: median seconds per model and call, and how many times as long as #833’s
Code
dt_table(opt_by_model)
Table: bench’s summary() of every mark
Code
dt_table(opt_summary, digits = 4)

How long does mcmc() take?

mcmc(), with 4 chains of 1000 warmup and 1000 kept draws, twice on the same model: the first call traces the model’s TensorFlow functions, which #833 also compiles with XLA, and the second reuses them.

Code
mcmc_summary <- bench_summary(mcmc_marks)
mcmc_by_model <- relative_medians(mcmc_timings, model, calls)

main_second_call <- mcmc_by_model |>
  filter(version == "main", calls == "second call on the same model")
mcmc_least <- main_second_call |>
  slice_min(times_as_long_as_833, with_ties = FALSE)
mcmc_most <- main_second_call |>
  slice_max(times_as_long_as_833, with_ties = FALSE)

ggplot(mcmc_timings, aes(x = seconds, y = version, colour = version)) +
  raincloud +
  facet_wrap(vars(model, calls), ncol = 2, scales = "free_x") +
  labs(x = "Seconds", y = NULL)

Each dot is one call, from every session. Above the dots, the curve is the density of the calls and the box marks their median and quartiles.

Timed by benchmark.R’s bench-mcmc-linear, bench-mcmc-multiple-linear, bench-mcmc-hierarchical-linear and bench-mcmc-eight-schools sections.

Over all models, main takes 1.00 times as long as #833 on a model’s first call, and 0.995 times as long on its second. Model by model, on the second call, main takes from 0.962 times as long as #833, on linear, to 1.25 times as long, on eight_schools.

Table: median seconds per model and call, and how many times as long as #833’s
Code
dt_table(mcmc_by_model)
Table: bench’s summary() of every mark
Code
dt_table(mcmc_summary, digits = 4)

Does #833 give more effective samples per second?

The bulk effective sample size (ESS) per second (Girolami and Calderhead 2011; Vehtari et al. 2021) of each model’s parameter with the smallest bulk ESS, as Hoffman and Gelman (2014) report it, from each session’s second mcmc() call, warmup included.

Code
# each model's posterior summary in each run, from every draw of every chain
# of its second mcmc() call
posterior <- lapply(seq_len(nrow(runs)), function(i) {
  draws <- runs$results[[i]]$mcmc_draws
  lapply(names(draws), function(model_name) {
    tibble(
      session = runs$session[i],
      version = runs$version[i],
      model = model_name,
      posterior::summarise_draws(
        draws[[model_name]],
        "mean",
        "sd",
        "mcse_mean",
        "rhat",
        "ess_bulk",
        "ess_tail"
      )
    )
  }) |>
    bind_rows()
}) |>
  bind_rows() |>
  mutate(model = factor(model, levels = model_levels))

second_calls <- mcmc_timings |>
  filter(call == 2) |>
  select(session, version, model, seconds)

efficiency <- posterior |>
  group_by(session, version, model) |>
  summarise(
    smallest_ess_bulk = min(ess_bulk),
    median_ess_bulk = median(ess_bulk),
    .groups = "drop"
  ) |>
  left_join(second_calls, by = c("session", "version", "model")) |>
  mutate(
    smallest_ess_per_second = smallest_ess_bulk / seconds,
    median_ess_per_second = median_ess_bulk / seconds
  )

efficiency_means <- efficiency |>
  group_by(model, version) |>
  summarise(
    smallest_ess_per_second = mean(smallest_ess_per_second),
    median_ess_per_second = mean(median_ess_per_second),
    .groups = "drop"
  )

# how many times main's mean smallest ESS per second #833 gives, per model
efficiency_vs_main <- efficiency_means |>
  group_by(model) |>
  summarise(
    times_main = smallest_ess_per_second[version == "#833"] /
      smallest_ess_per_second[version == "main"]
  )
efficiency_least <- slice_min(efficiency_vs_main, times_main, with_ties = FALSE)
efficiency_most <- slice_max(efficiency_vs_main, times_main, with_ties = FALSE)

# the widest spread between one version's sessions on one model
efficiency_spread <- efficiency |>
  group_by(model, version) |>
  summarise(
    times = max(smallest_ess_per_second) / min(smallest_ess_per_second),
    .groups = "drop"
  ) |>
  slice_max(times, with_ties = FALSE)

ggplot(efficiency, aes(x = smallest_ess_per_second, y = version, colour = version)) +
  geom_point(data = efficiency_means, size = 5, alpha = 0.25) +
  geom_point(position = jitter, size = 1.2) +
  version_scale +
  facet_wrap(~model, ncol = 1, scales = "free_x") +
  labs(x = "Smallest bulk ESS per second", y = NULL)

Each small dot is one session’s second mcmc() call. The large faint dot behind them is their mean.

Draws kept by benchmark.R’s mcmc-draws section, from the calls timed by its bench-mcmc-linear, bench-mcmc-multiple-linear, bench-mcmc-hierarchical-linear and bench-mcmc-eight-schools sections.

Averaged over sessions, #833 gives from 0.949 times main’s smallest bulk ESS per second, on linear, to 1.23 times main’s, on eight_schools. One version’s sessions differ by up to 24.5 times on the same model (CRAN on multiple_linear), so these differences between versions are within the noise.

Figure: median bulk ESS per second
Code
ggplot(efficiency, aes(x = median_ess_per_second, y = version, colour = version)) +
  geom_point(data = efficiency_means, size = 5, alpha = 0.25) +
  geom_point(position = jitter, size = 1.2) +
  version_scale +
  facet_wrap(~model, ncol = 1, scales = "free_x") +
  labs(x = "Median bulk ESS per second", y = NULL)

Each small dot is one session’s second mcmc() call. The large faint dot behind them is their mean.
Table: mean ESS per second
Code
dt_table(efficiency_means, digits = 1)

Do the versions sample the same posterior?

For each parameter, the difference between two versions’ posterior means, pooled over sessions, divided by the Monte Carlo standard error of that difference, \(z\) (Vehtari et al. 2021; Magnusson et al. 2025). If both versions sample the same posterior, each \(z\) is close to a standard normal draw.

Code
# Each session has a seed of its own, so the sessions are independent, and a
# version's pooled standard error is sqrt(sum(mcse^2)) / n over its n sessions
pooled <- posterior |>
  group_by(version, model, variable) |>
  summarise(
    sessions = n(),
    mean = mean(mean),
    se = sqrt(sum(mcse_mean^2)) / n(),
    .groups = "drop"
  )

pairs <- utils::combn(version_levels, 2)
agreement <- lapply(seq_len(ncol(pairs)), function(i) {
  reference <- filter(pooled, version == pairs[1, i])
  compared <- filter(pooled, version == pairs[2, i])
  inner_join(reference, compared, by = c("model", "variable"), suffix = c("_a", "_b")) |>
    mutate(
      comparison = paste(pairs[2, i], "vs", pairs[1, i]),
      z = (mean_b - mean_a) / sqrt(se_a^2 + se_b^2)
    )
}) |>
  bind_rows() |>
  mutate(comparison = factor(comparison, levels = unique(comparison)))

# the band all of a comparison's z fall inside with probability 0.95 when both
# versions sample the same posterior: a Bonferroni bound over its parameters
parameters_compared <- n_distinct(paste(agreement$model, agreement$variable))
band <- qnorm(1 - 0.05 / (2 * parameters_compared))

outside_band <- agreement |>
  group_by(comparison) |>
  summarise(outside = sum(abs(z) > band), largest_abs_z = max(abs(z)))
main_vs_833 <- filter(outside_band, comparison == "#833 vs main")

ggplot(agreement, aes(x = z, y = model)) +
  annotate(
    "rect",
    xmin = -band,
    xmax = band,
    ymin = -Inf,
    ymax = Inf,
    fill = "grey80",
    alpha = 0.5
  ) +
  geom_point(position = jitter, alpha = 0.7) +
  facet_wrap(~comparison, ncol = 1) +
  labs(x = "z", y = NULL)

Each dot is one parameter. The grey band is where every parameter’s z falls, with probability at least 0.95, when both versions sample the same posterior.

Draws kept by benchmark.R’s mcmc-draws section.

The grey band is \(\pm\) 3.12, which all 28 parameters’ \(z\) fall inside with probability at least 0.95 when two versions sample the same posterior, by a Bonferroni bound. Parameters outside it: main vs CRAN 0, #833 vs CRAN 0, #833 vs main 0; the largest \(|z|\) between #833 and main is 0.808.

Figure: posterior densities

Every draw of every chain, from each session’s second mcmc() call, pooled over sessions, with each version’s density overlaid in one panel per parameter.

Code
# every draw of every run, one row per draw and parameter
draws <- lapply(seq_len(nrow(runs)), function(i) {
  model_draws <- runs$results[[i]]$mcmc_draws
  lapply(names(model_draws), function(model_name) {
    posterior::as_draws_df(model_draws[[model_name]]) |>
      as.data.frame() |>
      pivot_longer(
        -c(.chain, .iteration, .draw),
        names_to = "variable",
        values_to = "value"
      ) |>
      mutate(
        session = runs$session[i],
        version = runs$version[i],
        model = model_name
      )
  }) |>
    bind_rows()
}) |>
  bind_rows()

# each version with its own line type as well as colour, so a line drawn on
# top of an identical one still shows
gg_densities <- function(name) {
  draws |>
    filter(model == name) |>
    mutate(variable = factor(variable, levels = unique(variable))) |>
    ggplot(aes(x = value, colour = version, linetype = version)) +
    geom_density(linewidth = 0.7) +
    scale_colour_manual(values = version_colours) +
    facet_wrap(~variable, scales = "free", ncol = 4) +
    labs(x = NULL, y = NULL, colour = NULL, linetype = NULL) +
    theme(legend.position = "top")
}

densities_height <- function(name) {
  1.5 + 2 * ceiling(parameter_count(name) / 4)
}
Code
gg_densities("linear")

Code
gg_densities("multiple_linear")

Code
gg_densities("hierarchical_linear")

Code
gg_densities("eight_schools")

Table: largest |z| per comparison and model
Code
agreement |>
  group_by(comparison, model) |>
  summarise(
    parameters = n(),
    largest_abs_z = max(abs(z)),
    parameter = variable[which.max(abs(z))],
    .groups = "drop"
  ) |>
  dt_table(digits = 2)

Do the chains converge?

The largest rank-normalised split-\(\hat{R}\) over a model’s parameters, from each session’s second mcmc() call (Vehtari et al. 2021).

Code
convergence <- posterior |>
  group_by(session, version, model) |>
  summarise(
    largest_rhat = max(rhat),
    smallest_ess_bulk = min(ess_bulk),
    .groups = "drop"
  )

convergence_means <- convergence |>
  group_by(model, version) |>
  summarise(
    largest_rhat = mean(largest_rhat),
    smallest_ess_bulk = mean(smallest_ess_bulk),
    .groups = "drop"
  )

# Vehtari et al. (2021) take chains to have converged when every parameter's
# R-hat is below this
rhat_threshold <- 1.01
runs_over_threshold <- convergence |>
  group_by(version) |>
  summarise(runs = sum(largest_rhat > rhat_threshold))
runs_per_version <- n_sessions * length(model_levels)
largest_rhat <- slice_max(posterior, rhat, with_ties = FALSE)

ggplot(convergence, aes(x = largest_rhat, y = version, colour = version)) +
  geom_point(data = convergence_means, size = 5, alpha = 0.25) +
  geom_point(position = jitter, size = 1.2) +
  version_scale +
  facet_wrap(~model, ncol = 1, scales = "free_x") +
  labs(x = "Largest R-hat", y = NULL)

Each small dot is one session’s second mcmc() call. The large faint dot behind them is their mean.

Draws kept by benchmark.R’s mcmc-draws section.

On every version, most runs’ largest \(\hat{R}\) is above 1.01: CRAN 17, main 18, #833 19 of 20 runs. The largest is 1.55, for multiple_linear’s int on CRAN.

Figure: traceplots

Every draw of each chain from session 1’s second mcmc() call on each model, numbered from the end of warmup, with a row of panels for each version.

Code
gg_trace <- function(name) {
  model_draws <- draws |>
    filter(session == 1, model == name) |>
    mutate(variable = factor(variable, levels = unique(variable)))
  # each parameter's panels share its range across the versions, so they can
  # be compared down a column
  parameter_ranges <- model_draws |>
    group_by(variable) |>
    reframe(value = range(value)) |>
    crossing(version = unique(model_draws$version)) |>
    mutate(.iteration = 1L)
  ggplot(model_draws, aes(x = .iteration, y = value, colour = factor(.chain))) +
    geom_line(linewidth = 0.2) +
    geom_blank(
      data = parameter_ranges,
      aes(x = .iteration, y = value),
      inherit.aes = FALSE
    ) +
    facet_wrap(
      vars(version, variable),
      ncol = n_distinct(model_draws$variable),
      scales = "free_y"
    ) +
    labs(x = "Draw", y = NULL, colour = "Chain")
}
Code
gg_trace("linear")

Code
gg_trace("multiple_linear")

Code
gg_trace("hierarchical_linear")

Code
gg_trace("eight_schools")

Table: largest R-hat and smallest bulk ESS of every run
Code
dt_table(convergence)

How it was measured

The versions, pinned to commits in run.R’s run-versions section:

Code
provenance |>
  distinct(version, greta_version, greta_sha, tensorflow_version, tfp_version, r_version) |>
  arrange(version) |>
  mutate(
    installed_from = versions[as.character(version)],
    # pak records a CRAN install's version in place of a commit, so only a
    # GitHub install gets a link to its commit
    installed_from = ifelse(
      grepl("^[0-9a-f]{40}$", greta_sha),
      paste0(
        "[",
        sub("@.*", "", installed_from),
        "@",
        substr(greta_sha, 1, 8),
        "](https://github.com/greta-dev/greta/commit/",
        greta_sha,
        ")"
      ),
      installed_from
    )
  ) |>
  select(
    version,
    `installed from` = installed_from,
    greta = greta_version,
    TensorFlow = tensorflow_version,
    TFP = tfp_version,
    R = r_version
  ) |>
  knitr::kable()
version installed from greta TensorFlow TFP R
CRAN greta@0.6.0 0.6.0 2.15.1 0.23.0 4.6.1
main greta-dev/greta@0a22f8c7 0.6.0.9000 2.21.0 0.25.0 4.6.1
#833 greta-dev/greta@9e2d57ea 0.6.0.9000 2.21.0 0.25.0 4.6.1
Code
python_stacks <- provenance |>
  distinct(version, tensorflow_version, tfp_version) |>
  group_by(tensorflow_version, tfp_version) |>
  summarise(
    versions = paste_and(as.character(sort(version))),
    .groups = "drop"
  )
python_stack_text <- paste0(
  python_stacks$versions,
  " ran on TensorFlow ",
  python_stacks$tensorflow_version,
  " and TensorFlow Probability ",
  python_stacks$tfp_version,
  collapse = "; "
)

xla_compiled <- lapply(seq_len(nrow(runs)), function(i) {
  compiled <- runs$results[[i]]$xla_compiled
  tibble(
    session = runs$session[i],
    version = runs$version[i],
    model = names(compiled),
    xla_compiled = unname(compiled)
  )
}) |>
  bind_rows()

xla_by_version <- xla_compiled |>
  group_by(version) |>
  summarise(compiled = paste(sort(unique(xla_compiled)), collapse = " or "))

sampler_iterations_values <- sampler_iterations_per_draw |>
  group_by(version) |>
  summarise(values = paste(sort(unique(value)), collapse = " or "))

CRAN ran on TensorFlow 2.15.1 and TensorFlow Probability 0.23.0; main and #833 ran on TensorFlow 2.21.0 and TensorFlow Probability 0.25.0. Each version ran in 5 sessions, on Darwin 25.5.0 arm64 with 8 cores. In each session, run.R’s run-sessions section ran benchmark.R once per version, each in a fresh R session, with the versions in a new random order.

Table: the order of each session
Code
runs |>
  select(session, position, version) |>
  arrange(session, position) |>
  dt_table()

Every model is built with compile = TRUE, and the settings are in benchmark.R’s benchmark-settings section. Whether XLA compiled the log-density function mcmc() used, read in benchmark-xla, over every model and session: CRAN FALSE, main FALSE, #833 TRUE. Sampler iterations per kept draw, measured in benchmark-sampler-iterations, in every session: CRAN 2, main 2, #833 2, so the same mcmc() arguments are the same work on each version.

The models

The models are from greta’s inst/examples/. Normal and log-normal distributions are written with their mean and standard deviation, as greta’s normal() and lognormal() take them; \(\text{Cauchy}^{+}\) is a Cauchy truncated to positive values.

A regression of each department’s rating in attitude on its number of complaints, with 3 parameters.

\[ \begin{aligned} \text{rating}_i &\sim \text{Normal}(\mu_i, \sigma), \quad i = 1, \dots, 30 \\ \mu_i &= \alpha + \beta \, \text{complaints}_i \\ \alpha, \beta &\sim \text{Normal}(0, 10) \\ \sigma &\sim \text{Cauchy}^{+}(0, 3) \end{aligned} \]

benchmark.R: model-linear
build_linear <- function() {
  int <- normal(0, 10)
  coef <- normal(0, 10)
  sd <- cauchy(0, 3, truncation = c(0, Inf))
  mu <- int + coef * attitude$complaints
  distribution(attitude$rating) <- normal(mu, sd)
  model(int, coef, sd, compile = TRUE)
}

The same rating on all six of attitude’s other columns, \(X\), through a matrix multiply, with 8 parameters.

\[ \begin{aligned} \text{rating}_i &\sim \text{Normal}(\mu_i, \sigma), \quad i = 1, \dots, 30 \\ \mu_i &= \alpha + \sum_{j = 1}^{6} X_{ij} \, \beta_j \\ \alpha, \beta_j &\sim \text{Normal}(0, 10) \\ \sigma &\sim \text{Cauchy}^{+}(0, 3) \end{aligned} \]

benchmark.R: model-multiple-linear
build_multiple_linear <- function() {
  design <- as.matrix(attitude[, 2:7])
  int <- normal(0, 10)
  coefs <- normal(0, 10, dim = ncol(design))
  sd <- cauchy(0, 3, truncation = c(0, Inf))
  mu <- int + design %*% coefs
  distribution(attitude$rating) <- normal(mu, sd)
  model(int, coefs, sd, compile = TRUE)
}

Sepal length on sepal width in iris, with an offset for each species, \(s[i]\) being flower \(i\)’s. Setosa’s offset is zero and the other two share a scale, \(\tau\). 6 parameters.

\[ \begin{aligned} \text{Sepal.Length}_i &\sim \text{Normal}(\mu_i, \sigma), \quad i = 1, \dots, 150 \\ \mu_i &= \alpha + \beta \, \text{Sepal.Width}_i + \gamma_{s[i]} \\ \gamma_1 &= 0, \qquad \gamma_2, \gamma_3 \sim \text{Normal}(0, \tau) \\ \tau &\sim \text{LogNormal}(0, 1) \\ \alpha, \beta &\sim \text{Normal}(0, 10) \\ \sigma &\sim \text{Cauchy}^{+}(0, 3) \end{aligned} \]

benchmark.R: model-hierarchical-linear
build_hierarchical_linear <- function() {
  int <- normal(0, 10)
  coef <- normal(0, 10)
  sd <- cauchy(0, 3, truncation = c(0, Inf))
  species_sd <- lognormal(0, 1)
  species_offset <- normal(0, species_sd, dim = 2)
  species_effect <- rbind(0, species_offset)
  species_id <- as.numeric(iris$Species)
  mu <- int + coef * iris$Sepal.Width + species_effect[species_id]
  distribution(iris$Sepal.Length) <- normal(mu, sd)
  model(int, coef, sd, species_sd, species_offset, compile = TRUE)
}

The effect of coaching in eight schools, \(y_j\), each measured with a known standard error, \(\sigma_j\). The scale of the school effects, \(\sigma_\eta\), is itself a parameter, which makes the posterior a funnel. 11 parameters.

\[ \begin{aligned} y_j &\sim \text{Normal}(\theta_j, \sigma_j), \quad j = 1, \dots, 8 \\ \theta_j &= \mu + \xi \, \eta_j \\ \eta_j &\sim \text{Normal}(0, \sigma_\eta) \\ \sigma_\eta &\sim \text{InverseGamma}(1, 1) \\ \mu &\sim \text{Normal}(0, 100) \\ \xi &\sim \text{Normal}(0, 5) \end{aligned} \]

benchmark.R: model-eight-schools
build_eight_schools <- function() {
  y <- c(28, 8, -3, 7, -1, 1, 18, 12)
  sigma_y <- c(15, 10, 16, 11, 9, 11, 10, 18)
  sigma_eta <- inverse_gamma(1, 1)
  eta <- normal(0, sigma_eta, dim = 8)
  mu_theta <- normal(0, 100)
  xi <- normal(0, 5)
  theta <- mu_theta + xi * eta
  distribution(y) <- normal(theta, sigma_y)
  model(sigma_eta, eta, mu_theta, xi, compile = TRUE)
}

The scripts

run.R installs each version and, in each session, runs benchmark.R for each version in a fresh R session of its own; benchmark.R is everything each version runs.

run.R
# Runs benchmark.R against CRAN, main and greta#833, in a fresh R session per
# version, in five sessions:
#
#   Rscript --quiet --vanilla posts/2026-10-06-jit-compile-i833/run.R
#
# Each session writes results/session-<n>.rds, and sessions already there are
# skipped, so a rerun after a crash carries on. Each `# ---- label ----` line
# starts a section that index.qmd shows by that label.

# ---- run-versions ----
# pin main and the branch to commits: both move, and the post links each one
versions <- c(
  CRAN = "greta@0.6.0",
  main = "greta-dev/greta@0a22f8c7e63ac32fa0926cdf67a85c9f69acd1d5",
  `#833` = "greta-dev/greta@9e2d57ea10da7d8aa19bffe6983ff9ca9d4facb2"
)
n_sessions <- 5
post_dir <- here::here("posts", "2026-10-06-jit-compile-i833")

# ---- run-sessions ----
dir.create(file.path(post_dir, "results"), showWarnings = FALSE)

for (session in seq_len(n_sessions)) {
  session_file <- file.path(
    post_dir,
    "results",
    sprintf("session-%d.rds", session)
  )
  if (file.exists(session_file)) {
    next
  }

  # a fresh order each session, so no version is always measured first or last
  order <- sample(names(versions))

  # cross::run_versions() installs each version into a library of its own and
  # evaluates the expression in a fresh R session against it. That session
  # cannot see this one's variables, so the script's path and the session
  # number reach it as environment variables: args_callr is passed on to
  # callr::r(), whose `env` sets variables for the child process (see ?callr::r
  # and ?callr::rcmd_safe_env, whose defaults are kept).
  measured <- cross::run_versions(
    source(Sys.getenv("GRETA_BENCH_SCRIPT"), local = TRUE)$value,
    pkgs = unname(versions[order]),
    args_callr = list(
      env = c(
        callr::rcmd_safe_env(),
        GRETA_BENCH_SCRIPT = file.path(post_dir, "benchmark.R"),
        GRETA_BENCH_SESSION = session
      )
    )
  )

  saveRDS(
    list(
      session = session,
      versions = versions,
      order = order,
      results = setNames(measured$result, order)
    ),
    session_file,
    compress = "xz"
  )
}
benchmark.R
# What each version of greta runs, in a fresh R session of its own: run.R
# sources it once per version per session, and keeps the list it ends with.
# Each `# ---- label ----` line starts a section that index.qmd shows by that
# label.
#
# Run it alone to try it against whichever greta is installed:
#
#   Rscript --quiet --vanilla posts/2026-10-06-jit-compile-i833/benchmark.R

# ---- benchmark-setup ----
library(greta)
library(bench)
session <- as.integer(Sys.getenv("GRETA_BENCH_SESSION", "1"))
session_seed <- 2026 - 10 - 6 + session

# ---- benchmark-settings ----
# warmup and n_samples are mcmc()'s arguments, so they count draws; the
# *_calls settings count timed calls. Every model is built with
# compile = TRUE: #833 compiles it with XLA, and CRAN and main accept the
# argument and do nothing with it.
bench_settings <- list(
  warmup = 1000,
  n_samples = 1000,
  chains = 4,
  n_cores = 4,
  build_calls = 5,
  opt_calls = 5,
  mcmc_calls = 2
)

# ---- model-linear ----
build_linear <- function() {
  int <- normal(0, 10)
  coef <- normal(0, 10)
  sd <- cauchy(0, 3, truncation = c(0, Inf))
  mu <- int + coef * attitude$complaints
  distribution(attitude$rating) <- normal(mu, sd)
  model(int, coef, sd, compile = TRUE)
}

# ---- model-multiple-linear ----
build_multiple_linear <- function() {
  design <- as.matrix(attitude[, 2:7])
  int <- normal(0, 10)
  coefs <- normal(0, 10, dim = ncol(design))
  sd <- cauchy(0, 3, truncation = c(0, Inf))
  mu <- int + design %*% coefs
  distribution(attitude$rating) <- normal(mu, sd)
  model(int, coefs, sd, compile = TRUE)
}

# ---- model-hierarchical-linear ----
build_hierarchical_linear <- function() {
  int <- normal(0, 10)
  coef <- normal(0, 10)
  sd <- cauchy(0, 3, truncation = c(0, Inf))
  species_sd <- lognormal(0, 1)
  species_offset <- normal(0, species_sd, dim = 2)
  species_effect <- rbind(0, species_offset)
  species_id <- as.numeric(iris$Species)
  mu <- int + coef * iris$Sepal.Width + species_effect[species_id]
  distribution(iris$Sepal.Length) <- normal(mu, sd)
  model(int, coef, sd, species_sd, species_offset, compile = TRUE)
}

# ---- model-eight-schools ----
build_eight_schools <- function() {
  y <- c(28, 8, -3, 7, -1, 1, 18, 12)
  sigma_y <- c(15, 10, 16, 11, 9, 11, 10, 18)
  sigma_eta <- inverse_gamma(1, 1)
  eta <- normal(0, sigma_eta, dim = 8)
  mu_theta <- normal(0, 100)
  xi <- normal(0, 5)
  theta <- mu_theta + xi * eta
  distribution(y) <- normal(theta, sigma_y)
  model(sigma_eta, eta, mu_theta, xi, compile = TRUE)
}

# ---- benchmark-sampler-iterations ----
# A random walk on a target far wider than its steps accepts every proposal, so
# the variance between kept draws over one step's variance counts the sampler
# iterations between them. It is also the session's first mcmc() call, so
# TensorFlow's start-up costs fall here rather than on the first model.
step_sd <- 0.1
x <- normal(0, 1e6)
walk <- mcmc(
  model(x),
  sampler = rwmh(epsilon = step_sd, diag_sd = 1),
  warmup = 0,
  n_samples = 4000,
  thin = 1,
  chains = 1,
  initial_values = initials(x = 0),
  verbose = FALSE
)
sampler_iterations_per_draw <- round(
  var(diff(as.vector(walk[[1]]))) / step_sd^2
)

# ---- bench-build-linear ----
# mark()'s `iterations` is how many times it calls the expression
build_linear
bench_build_linear <- mark(
  build_linear(),
  iterations = bench_settings$build_calls,
  check = FALSE,
  memory = FALSE,
  filter_gc = FALSE
)

# ---- bench-build-multiple-linear ----
build_multiple_linear
bench_build_multiple_linear <- mark(
  build_multiple_linear(),
  iterations = bench_settings$build_calls,
  check = FALSE,
  memory = FALSE,
  filter_gc = FALSE
)

# ---- bench-build-hierarchical-linear ----
build_hierarchical_linear
bench_build_hierarchical_linear <- mark(
  build_hierarchical_linear(),
  iterations = bench_settings$build_calls,
  check = FALSE,
  memory = FALSE,
  filter_gc = FALSE
)

# ---- bench-build-eight-schools ----
build_eight_schools
bench_build_eight_schools <- mark(
  build_eight_schools(),
  iterations = bench_settings$build_calls,
  check = FALSE,
  memory = FALSE,
  filter_gc = FALSE
)

# ---- bench-opt ----
# opt() draws its starting values from R's random numbers, so each model's
# calls start from the same seed
# ---- bench-opt-linear ----
build_linear
linear_for_opt <- build_linear()
set.seed(session_seed)
bench_opt_linear <- mark(
  opt(linear_for_opt),
  iterations = bench_settings$opt_calls,
  check = FALSE,
  memory = FALSE,
  filter_gc = FALSE
)

# ---- bench-opt-multiple-linear ----
build_multiple_linear
multiple_linear_for_opt <- build_multiple_linear()
set.seed(session_seed)
bench_opt_multiple_linear <- mark(
  opt(multiple_linear_for_opt),
  iterations = bench_settings$opt_calls,
  check = FALSE,
  memory = FALSE,
  filter_gc = FALSE
)

# ---- bench-opt-hierarchical-linear ----
build_hierarchical_linear
hierarchical_linear_for_opt <- build_hierarchical_linear()
set.seed(session_seed)
bench_opt_hierarchical_linear <- mark(
  opt(hierarchical_linear_for_opt),
  iterations = bench_settings$opt_calls,
  check = FALSE,
  memory = FALSE,
  filter_gc = FALSE
)

# ---- bench-opt-eight-schools ----
build_eight_schools
eight_schools_for_opt <- build_eight_schools()
set.seed(session_seed)
bench_opt_eight_schools <- mark(
  opt(eight_schools_for_opt),
  iterations = bench_settings$opt_calls,
  check = FALSE,
  memory = FALSE,
  filter_gc = FALSE
)

# ---- bench-mcmc ----
# Both of a model's calls are on the same model: the first traces its
# TensorFlow functions and the second reuses them. Each `draws_` object keeps
# the second call's draws.

# ---- bench-mcmc-linear ----
build_linear
linear_for_mcmc <- build_linear()
set.seed(session_seed)
bench_mcmc_linear <- mark(
  draws_linear <- mcmc(
    linear_for_mcmc,
    warmup = bench_settings$warmup,
    n_samples = bench_settings$n_samples,
    chains = bench_settings$chains,
    n_cores = bench_settings$n_cores,
    verbose = FALSE
  ),
  iterations = bench_settings$mcmc_calls,
  check = FALSE,
  memory = FALSE,
  filter_gc = FALSE
)

# ---- bench-mcmc-multiple-linear ----
build_multiple_linear
multiple_linear_for_mcmc <- build_multiple_linear()
set.seed(session_seed)
bench_mcmc_multiple_linear <- mark(
  draws_multiple_linear <- mcmc(
    multiple_linear_for_mcmc,
    warmup = bench_settings$warmup,
    n_samples = bench_settings$n_samples,
    chains = bench_settings$chains,
    n_cores = bench_settings$n_cores,
    verbose = FALSE
  ),
  iterations = bench_settings$mcmc_calls,
  check = FALSE,
  memory = FALSE,
  filter_gc = FALSE
)

# ---- bench-mcmc-hierarchical-linear ----
build_hierarchical_linear
hierarchical_linear_for_mcmc <- build_hierarchical_linear()
set.seed(session_seed)
bench_mcmc_hierarchical_linear <- mark(
  draws_hierarchical_linear <- mcmc(
    hierarchical_linear_for_mcmc,
    warmup = bench_settings$warmup,
    n_samples = bench_settings$n_samples,
    chains = bench_settings$chains,
    n_cores = bench_settings$n_cores,
    verbose = FALSE
  ),
  iterations = bench_settings$mcmc_calls,
  check = FALSE,
  memory = FALSE,
  filter_gc = FALSE
)

# ---- bench-mcmc-eight-schools ----
build_eight_schools
eight_schools_for_mcmc <- build_eight_schools()
set.seed(session_seed)
bench_mcmc_eight_schools <- mark(
  draws_eight_schools <- mcmc(
    eight_schools_for_mcmc,
    warmup = bench_settings$warmup,
    n_samples = bench_settings$n_samples,
    chains = bench_settings$chains,
    n_cores = bench_settings$n_cores,
    verbose = FALSE
  ),
  iterations = bench_settings$mcmc_calls,
  check = FALSE,
  memory = FALSE,
  filter_gc = FALSE
)

# ---- benchmark-xla ----
# Whether each version set jit_compile on the log-density function mcmc()
# used, read from the tf_function: TRUE where XLA compiled it, and NA where the
# version has no such function to ask.
jit_compiled <- function(model) {
  tryCatch(
    isTRUE(reticulate::py_to_r(model$dag$tf_log_prob_function$`_jit_compile`)),
    error = function(error) NA
  )
}
xla_compiled <- c(
  linear = jit_compiled(linear_for_mcmc),
  multiple_linear = jit_compiled(multiple_linear_for_mcmc),
  hierarchical_linear = jit_compiled(hierarchical_linear_for_mcmc),
  eight_schools = jit_compiled(eight_schools_for_mcmc)
)

# ---- mcmc-draws ----
# Every draw of every chain from each model's second call. A posterior draws
# array holds just the draws: the greta_mcmc_list mcmc() returns carries the
# model and its TensorFlow objects, which cannot be saved.
mcmc_draws <- list(
  linear = posterior::as_draws_array(draws_linear),
  multiple_linear = posterior::as_draws_array(draws_multiple_linear),
  hierarchical_linear = posterior::as_draws_array(draws_hierarchical_linear),
  eight_schools = posterior::as_draws_array(draws_eight_schools)
)

# ---- benchmark-provenance ----
session_info <- sessioninfo::session_info()
# greta_sitrep() reports through messages, so capture those
greta_sitrep_report <- utils::capture.output(greta_sitrep(), type = "message")

greta_sha <- packageDescription("greta")$RemoteSha
provenance <- data.frame(
  greta_version = as.character(packageVersion("greta")),
  greta_sha = if (is.null(greta_sha)) NA_character_ else greta_sha,
  r_version = paste(R.version$major, R.version$minor, sep = "."),
  tensorflow_version = tensorflow::tf$version$VERSION,
  tfp_version = reticulate::import("tensorflow_probability")$`__version__`,
  machine = paste(
    Sys.info()[c("sysname", "release", "machine")],
    collapse = " "
  ),
  cores = parallel::detectCores()
)

# ---- benchmark-results ----
# the value source() returns to run.R: every bench_mark object as mark() made
# it, and the summaries beside them
list(
  session_seed = session_seed,
  bench_settings = bench_settings,
  provenance = provenance,
  session_info = session_info,
  greta_sitrep_report = greta_sitrep_report,
  sampler_iterations_per_draw = sampler_iterations_per_draw,
  xla_compiled = xla_compiled,
  build = list(
    linear = bench_build_linear,
    multiple_linear = bench_build_multiple_linear,
    hierarchical_linear = bench_build_hierarchical_linear,
    eight_schools = bench_build_eight_schools
  ),
  opt = list(
    linear = bench_opt_linear,
    multiple_linear = bench_opt_multiple_linear,
    hierarchical_linear = bench_opt_hierarchical_linear,
    eight_schools = bench_opt_eight_schools
  ),
  mcmc = list(
    linear = bench_mcmc_linear,
    multiple_linear = bench_mcmc_multiple_linear,
    hierarchical_linear = bench_mcmc_hierarchical_linear,
    eight_schools = bench_mcmc_eight_schools
  ),
  mcmc_draws = mcmc_draws
)

Session details

What each version reported about itself in session 1, from benchmark.R’s benchmark-provenance section.

Code
# a saved session_info() prints with sessioninfo's print method, which needs
# sessioninfo's namespace loaded
invisible(loadNamespace("sessioninfo"))

session_one <- runs |>
  filter(session == 1) |>
  arrange(version)
greta_sitrep()
Code
writeLines(session_one$results[[which(session_one$version == "CRAN")]]$greta_sitrep_report)
ℹ checking if python available
✔ python (v3.11) available

ℹ checking if TensorFlow available
✔ TensorFlow (v2.15.1) available

ℹ checking if TensorFlow Probability available
✔ TensorFlow Probability (v0.23.0) available

ℹ greta conda environment: not used (managed (uv) environment active)
• backend: "managed (uv) environment"
• selected via: default
✔ offline-ready: uv cache present, offline mode will engage
ℹ All dependencies available; greta is ready to use!
sessioninfo::session_info()
Code
session_one$results[[which(session_one$version == "CRAN")]]$session_info
─ Session info ───────────────────────────────────────────────────────────────
 setting  value
 version  R version 4.6.1 (2026-06-24)
 os       macOS Tahoe 26.5.2
 system   aarch64, darwin23
 ui       X11
 language (EN)
 collate  en_AU.UTF-8
 ctype    en_AU.UTF-8
 tz       Australia/Hobart
 date     2026-10-06
 pandoc   3.10 @ /opt/homebrew/bin/pandoc
 quarto   1.9.36 @ /usr/local/bin/quarto

─ Packages ───────────────────────────────────────────────────────────────────
 package        * version  date (UTC) lib source
 abind            1.4-8    2024-09-12 [1] CRAN (R 4.6.0)
 backports        1.5.1    2026-04-03 [1] CRAN (R 4.6.0)
 base64enc        0.1-6    2026-02-02 [1] CRAN (R 4.6.0)
 bench          * 1.1.4    2025-01-16 [3] CRAN (R 4.6.0)
 callr            3.8.0    2026-06-05 [1] CRAN (R 4.6.0)
 checkmate        2.3.4    2026-02-03 [2] CRAN (R 4.6.0)
 cli              3.6.6    2026-04-09 [1] CRAN (R 4.6.0)
 coda             0.19-4.1 2024-01-31 [1] CRAN (R 4.6.0)
 codetools        0.2-20   2024-03-31 [3] CRAN (R 4.6.1)
 crayon           1.5.3    2024-06-20 [1] CRAN (R 4.6.0)
 digest           0.6.39   2025-11-19 [1] CRAN (R 4.6.0)
 distributional   0.9.0    2026-09-08 [2] CRAN (R 4.6.1)
 future           1.76.0   2026-09-24 [1] CRAN (R 4.6.1)
 generics         0.1.4    2025-05-09 [2] CRAN (R 4.6.0)
 globals          0.19.1   2026-03-13 [1] CRAN (R 4.6.0)
 glue             1.8.1    2026-04-17 [1] CRAN (R 4.6.0)
 greta          * 0.6.0    2026-07-20 [1] CRAN (R 4.6.1)
 hms              1.1.4    2025-10-17 [1] CRAN (R 4.6.0)
 jsonlite         2.0.0    2025-03-27 [1] CRAN (R 4.6.0)
 lattice          0.23-1   2026-08-12 [3] CRAN (R 4.6.1)
 lifecycle        1.0.5    2026-01-08 [1] CRAN (R 4.6.0)
 listenv          1.1.0    2026-10-04 [1] CRAN (R 4.6.1)
 magrittr         2.0.5    2026-04-04 [1] CRAN (R 4.6.0)
 Matrix           1.7-6    2026-07-25 [3] CRAN (R 4.6.1)
 parallelly       1.48.0   2026-06-29 [1] CRAN (R 4.6.1)
 pillar           1.11.1   2025-09-17 [2] CRAN (R 4.6.0)
 pkgconfig        2.0.3    2019-09-22 [1] CRAN (R 4.6.0)
 png              0.1-9    2026-03-15 [1] CRAN (R 4.6.0)
 posterior        1.7.0    2026-04-01 [2] CRAN (R 4.6.0)
 prettyunits      1.2.0    2023-09-24 [1] CRAN (R 4.6.0)
 processx         3.9.0    2026-04-22 [1] CRAN (R 4.6.0)
 progress         1.2.3    2023-12-06 [1] CRAN (R 4.6.0)
 R6               2.6.1    2025-02-15 [1] CRAN (R 4.6.0)
 Rcpp             1.1.2    2026-07-05 [1] CRAN (R 4.6.1)
 reticulate       1.47.0   2026-09-03 [1] CRAN (R 4.6.1)
 rlang            1.3.0    2026-07-05 [1] CRAN (R 4.6.1)
 sessioninfo      1.2.4    2026-06-04 [3] CRAN (R 4.6.0)
 tensorA          0.36.2.1 2023-12-13 [2] CRAN (R 4.6.0)
 tensorflow       2.20.0   2025-08-22 [1] CRAN (R 4.6.0)
 tfautograph      0.3.2    2021-09-17 [1] CRAN (R 4.6.0)
 tfruns           1.5.4    2025-08-20 [1] CRAN (R 4.6.0)
 tibble           3.3.1    2026-01-11 [2] CRAN (R 4.6.0)
 vctrs            0.7.3    2026-04-11 [1] CRAN (R 4.6.0)
 whisker          0.4.1    2022-12-05 [1] CRAN (R 4.6.0)
 withr            3.0.3    2026-06-19 [1] CRAN (R 4.6.0)

 [1] /private/var/folders/cz/hrp33k3s0jlcb_gg9snlqh580000gq/T/RtmpwgRNQj/file81f7d58343f
 [2] /Users/nick_1/Library/R/arm64/4.6/library
 [3] /Library/Frameworks/R.framework/Versions/4.6/Resources/library
 * ── Packages attached to the search path.

─ Python configuration ───────────────────────────────────────────────────────
 /Users/nick_1/.cache/uv/archive-v0/ogasBA2nxVj6KqkBWNO5g/bin/python
 /Users/nick_1/.local/share/uv/python/cpython-3.11.15-macos-aarch64-none/lib/libpython3.11.dylib
 /Users/nick_1/.cache/uv/archive-v0/ogasBA2nxVj6KqkBWNO5g:/Users/nick_1/.cache/uv/archive-v0/ogasBA2nxVj6KqkBWNO5g
 /Users/nick_1/.local/share/uv/python/cpython-3.11.15-macos-aarch64-none/lib/python311.zip:/Users/nick_1/.local/share/uv/python/cpython-3.11.15-macos-aarch64-none/lib/python3.11:/Users/nick_1/.local/share/uv/python/cpython-3.11.15-macos-aarch64-none/lib/python3.11/lib-dynload:/Users/nick_1/.cache/uv/archive-v0/ogasBA2nxVj6KqkBWNO5g/lib/python3.11/site-packages
 /Users/nick_1/.cache/uv/archive-v0/ogasBA2nxVj6KqkBWNO5g
 /Users/nick_1/.cache/uv/archive-v0/ogasBA2nxVj6KqkBWNO5g
 /Users/nick_1/.local/share/uv/python/cpython-3.11.15-macos-aarch64-none
 /Users/nick_1/.cache/uv/archive-v0/ogasBA2nxVj6KqkBWNO5g
 /Users/nick_1/.cache/uv/archive-v0/ogasBA2nxVj6KqkBWNO5g/bin/activate_this.py
 /Users/nick_1/.cache/uv/archive-v0/ogasBA2nxVj6KqkBWNO5g/bin/python
 /Users/nick_1/.local/share/uv/python/cpython-3.11.15-macos-aarch64-none/bin/python3.11
 3.11.15 (main, Mar 20 2026, 00:41:54) [Clang 22.1.1 ]
 3, 11
 64bit
 FALSE
 False
 /Users/nick_1/.cache/uv/archive-v0/ogasBA2nxVj6KqkBWNO5g/lib/python3.11/site-packages/numpy, 1                                                                                          , 26                                                                                         , 4                                                                                          
 tensorflow
 /Users/nick_1/.cache/uv/archive-v0/ogasBA2nxVj6KqkBWNO5g/lib/python3.11/site-packages/tensorflow
 TRUE
 /Users/nick_1/.cache/uv/archive-v0/ogasBA2nxVj6KqkBWNO5g/bin/python
 py_require()
 TRUE

──────────────────────────────────────────────────────────────────────────────
greta_sitrep()
Code
writeLines(session_one$results[[which(session_one$version == "main")]]$greta_sitrep_report)
ℹ checking if python available
✔ python (v3.12) available

ℹ checking if TensorFlow available
✔ TensorFlow (v2.21.0) available

ℹ checking if TensorFlow Probability available
✔ TensorFlow Probability (v0.25.0) available

ℹ greta conda environment: not used (managed (uv) environment active)
• backend: "managed (uv) environment"
• selected via: default
✔ offline-ready: `UV_OFFLINE`=1 is set and the uv cache is populated
ℹ All dependencies available; greta is ready to use!
sessioninfo::session_info()
Code
session_one$results[[which(session_one$version == "main")]]$session_info
─ Session info ───────────────────────────────────────────────────────────────
 setting  value
 version  R version 4.6.1 (2026-06-24)
 os       macOS Tahoe 26.5.2
 system   aarch64, darwin23
 ui       X11
 language (EN)
 collate  en_AU.UTF-8
 ctype    en_AU.UTF-8
 tz       Australia/Hobart
 date     2026-10-06
 pandoc   3.10 @ /opt/homebrew/bin/pandoc
 quarto   1.9.36 @ /usr/local/bin/quarto

─ Packages ───────────────────────────────────────────────────────────────────
 package        * version    date (UTC) lib source
 abind            1.4-8      2024-09-12 [1] CRAN (R 4.6.0)
 backports        1.5.1      2026-04-03 [1] CRAN (R 4.6.0)
 base64enc        0.1-6      2026-02-02 [1] CRAN (R 4.6.0)
 bench          * 1.1.4      2025-01-16 [3] CRAN (R 4.6.0)
 callr            3.8.0      2026-06-05 [1] CRAN (R 4.6.0)
 checkmate        2.3.4      2026-02-03 [2] CRAN (R 4.6.0)
 cli              3.6.6      2026-04-09 [1] CRAN (R 4.6.0)
 coda             0.19-4.1   2024-01-31 [1] CRAN (R 4.6.0)
 codetools        0.2-20     2024-03-31 [3] CRAN (R 4.6.1)
 crayon           1.5.3      2024-06-20 [1] CRAN (R 4.6.0)
 digest           0.6.39     2025-11-19 [1] CRAN (R 4.6.0)
 distributional   0.9.0      2026-09-08 [2] CRAN (R 4.6.1)
 future           1.76.0     2026-09-24 [1] CRAN (R 4.6.1)
 generics         0.1.4      2025-05-09 [2] CRAN (R 4.6.0)
 globals          0.19.1     2026-03-13 [1] CRAN (R 4.6.0)
 glue             1.8.1      2026-04-17 [1] CRAN (R 4.6.0)
 greta          * 0.6.0.9000 2026-10-06 [1] Github (greta-dev/greta@0a22f8c)
 hms              1.1.4      2025-10-17 [1] CRAN (R 4.6.0)
 jsonlite         2.0.0      2025-03-27 [1] CRAN (R 4.6.0)
 lattice          0.23-1     2026-08-12 [3] CRAN (R 4.6.1)
 lifecycle        1.0.5      2026-01-08 [1] CRAN (R 4.6.0)
 listenv          1.1.0      2026-10-04 [1] CRAN (R 4.6.1)
 magrittr         2.0.5      2026-04-04 [1] CRAN (R 4.6.0)
 Matrix           1.7-6      2026-07-25 [3] CRAN (R 4.6.1)
 parallelly       1.48.0     2026-06-29 [1] CRAN (R 4.6.1)
 pillar           1.11.1     2025-09-17 [2] CRAN (R 4.6.0)
 pkgconfig        2.0.3      2019-09-22 [1] CRAN (R 4.6.0)
 png              0.1-9      2026-03-15 [1] CRAN (R 4.6.0)
 posterior        1.7.0      2026-04-01 [2] CRAN (R 4.6.0)
 prettyunits      1.2.0      2023-09-24 [1] CRAN (R 4.6.0)
 processx         3.9.0      2026-04-22 [1] CRAN (R 4.6.0)
 progress         1.2.3      2023-12-06 [1] CRAN (R 4.6.0)
 R6               2.6.1      2025-02-15 [1] CRAN (R 4.6.0)
 Rcpp             1.1.2      2026-07-05 [1] CRAN (R 4.6.1)
 reticulate       1.47.0     2026-09-03 [1] CRAN (R 4.6.1)
 rlang            1.3.0      2026-07-05 [1] CRAN (R 4.6.1)
 sessioninfo      1.2.4      2026-06-04 [3] CRAN (R 4.6.0)
 tensorA          0.36.2.1   2023-12-13 [2] CRAN (R 4.6.0)
 tensorflow       2.20.0     2025-08-22 [1] CRAN (R 4.6.0)
 tfautograph      0.3.2      2021-09-17 [1] CRAN (R 4.6.0)
 tfruns           1.5.4      2025-08-20 [1] CRAN (R 4.6.0)
 tibble           3.3.1      2026-01-11 [2] CRAN (R 4.6.0)
 vctrs            0.7.3      2026-04-11 [1] CRAN (R 4.6.0)
 whisker          0.4.1      2022-12-05 [1] CRAN (R 4.6.0)
 withr            3.0.3      2026-06-19 [1] CRAN (R 4.6.0)

 [1] /private/var/folders/cz/hrp33k3s0jlcb_gg9snlqh580000gq/T/RtmpwgRNQj/file81f52d630ac
 [2] /Users/nick_1/Library/R/arm64/4.6/library
 [3] /Library/Frameworks/R.framework/Versions/4.6/Resources/library
 * ── Packages attached to the search path.

─ Python configuration ───────────────────────────────────────────────────────
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/bin/python
 /Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none/lib/lib/libpython3.12.dylib
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF:/Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF
 /Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none/lib/python312.zip:/Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none/lib/python3.12:/Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none/lib/python3.12/lib-dynload:/Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/lib/python3.12/site-packages
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF
 /Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/bin/activate_this.py
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/bin/python
 /Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none/bin/python3.12
 3.12.13 (main, Mar 20 2026, 00:20:20) [Clang 22.1.1 ]
 3, 12
 64bit
 FALSE
 False
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/lib/python3.12/site-packages/numpy, 2                                                                                          , 5                                                                                          , 3                                                                                          
 tensorflow
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/lib/python3.12/site-packages/tensorflow
 TRUE
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/bin/python
 py_require()
 TRUE

──────────────────────────────────────────────────────────────────────────────
greta_sitrep()
Code
writeLines(session_one$results[[which(session_one$version == "#833")]]$greta_sitrep_report)
ℹ checking if python available
✔ python (v3.12) available

ℹ checking if TensorFlow available
✔ TensorFlow (v2.21.0) available

ℹ checking if TensorFlow Probability available
✔ TensorFlow Probability (v0.25.0) available

ℹ greta conda environment: not used (managed (uv) environment active)
• backend: "managed (uv) environment"
• selected via: default
✔ offline-ready: `UV_OFFLINE`=1 is set and the uv cache is populated
ℹ All dependencies available; greta is ready to use!
sessioninfo::session_info()
Code
session_one$results[[which(session_one$version == "#833")]]$session_info
─ Session info ───────────────────────────────────────────────────────────────
 setting  value
 version  R version 4.6.1 (2026-06-24)
 os       macOS Tahoe 26.5.2
 system   aarch64, darwin23
 ui       X11
 language (EN)
 collate  en_AU.UTF-8
 ctype    en_AU.UTF-8
 tz       Australia/Hobart
 date     2026-10-06
 pandoc   3.10 @ /opt/homebrew/bin/pandoc
 quarto   1.9.36 @ /usr/local/bin/quarto

─ Packages ───────────────────────────────────────────────────────────────────
 package        * version    date (UTC) lib source
 abind            1.4-8      2024-09-12 [1] CRAN (R 4.6.0)
 backports        1.5.1      2026-04-03 [1] CRAN (R 4.6.0)
 base64enc        0.1-6      2026-02-02 [1] CRAN (R 4.6.0)
 bench          * 1.1.4      2025-01-16 [3] CRAN (R 4.6.0)
 callr            3.8.0      2026-06-05 [1] CRAN (R 4.6.0)
 checkmate        2.3.4      2026-02-03 [2] CRAN (R 4.6.0)
 cli              3.6.6      2026-04-09 [1] CRAN (R 4.6.0)
 coda             0.19-4.1   2024-01-31 [1] CRAN (R 4.6.0)
 codetools        0.2-20     2024-03-31 [3] CRAN (R 4.6.1)
 crayon           1.5.3      2024-06-20 [1] CRAN (R 4.6.0)
 digest           0.6.39     2025-11-19 [1] CRAN (R 4.6.0)
 distributional   0.9.0      2026-09-08 [2] CRAN (R 4.6.1)
 future           1.76.0     2026-09-24 [1] CRAN (R 4.6.1)
 generics         0.1.4      2025-05-09 [2] CRAN (R 4.6.0)
 globals          0.19.1     2026-03-13 [1] CRAN (R 4.6.0)
 glue             1.8.1      2026-04-17 [1] CRAN (R 4.6.0)
 greta          * 0.6.0.9000 2026-10-06 [1] Github (greta-dev/greta@9e2d57e)
 hms              1.1.4      2025-10-17 [1] CRAN (R 4.6.0)
 jsonlite         2.0.0      2025-03-27 [1] CRAN (R 4.6.0)
 lattice          0.23-1     2026-08-12 [3] CRAN (R 4.6.1)
 lifecycle        1.0.5      2026-01-08 [1] CRAN (R 4.6.0)
 listenv          1.1.0      2026-10-04 [1] CRAN (R 4.6.1)
 magrittr         2.0.5      2026-04-04 [1] CRAN (R 4.6.0)
 Matrix           1.7-6      2026-07-25 [3] CRAN (R 4.6.1)
 parallelly       1.48.0     2026-06-29 [1] CRAN (R 4.6.1)
 pillar           1.11.1     2025-09-17 [2] CRAN (R 4.6.0)
 pkgconfig        2.0.3      2019-09-22 [1] CRAN (R 4.6.0)
 png              0.1-9      2026-03-15 [1] CRAN (R 4.6.0)
 posterior        1.7.0      2026-04-01 [2] CRAN (R 4.6.0)
 prettyunits      1.2.0      2023-09-24 [1] CRAN (R 4.6.0)
 processx         3.9.0      2026-04-22 [1] CRAN (R 4.6.0)
 progress         1.2.3      2023-12-06 [1] CRAN (R 4.6.0)
 R6               2.6.1      2025-02-15 [1] CRAN (R 4.6.0)
 Rcpp             1.1.2      2026-07-05 [1] CRAN (R 4.6.1)
 reticulate       1.47.0     2026-09-03 [1] CRAN (R 4.6.1)
 rlang            1.3.0      2026-07-05 [1] CRAN (R 4.6.1)
 sessioninfo      1.2.4      2026-06-04 [3] CRAN (R 4.6.0)
 tensorA          0.36.2.1   2023-12-13 [2] CRAN (R 4.6.0)
 tensorflow       2.20.0     2025-08-22 [1] CRAN (R 4.6.0)
 tfautograph      0.3.2      2021-09-17 [1] CRAN (R 4.6.0)
 tfruns           1.5.4      2025-08-20 [1] CRAN (R 4.6.0)
 tibble           3.3.1      2026-01-11 [2] CRAN (R 4.6.0)
 vctrs            0.7.3      2026-04-11 [1] CRAN (R 4.6.0)
 whisker          0.4.1      2022-12-05 [1] CRAN (R 4.6.0)
 withr            3.0.3      2026-06-19 [1] CRAN (R 4.6.0)

 [1] /private/var/folders/cz/hrp33k3s0jlcb_gg9snlqh580000gq/T/RtmpwgRNQj/file81f6c1596b0
 [2] /Users/nick_1/Library/R/arm64/4.6/library
 [3] /Library/Frameworks/R.framework/Versions/4.6/Resources/library
 * ── Packages attached to the search path.

─ Python configuration ───────────────────────────────────────────────────────
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/bin/python
 /Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none/lib/lib/libpython3.12.dylib
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF:/Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF
 /Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none/lib/python312.zip:/Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none/lib/python3.12:/Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none/lib/python3.12/lib-dynload:/Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/lib/python3.12/site-packages
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF
 /Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/bin/activate_this.py
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/bin/python
 /Users/nick_1/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none/bin/python3.12
 3.12.13 (main, Mar 20 2026, 00:20:20) [Clang 22.1.1 ]
 3, 12
 64bit
 FALSE
 False
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/lib/python3.12/site-packages/numpy, 2                                                                                          , 5                                                                                          , 3                                                                                          
 tensorflow
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/lib/python3.12/site-packages/tensorflow
 TRUE
 /Users/nick_1/.cache/uv/archive-v0/L7vrsM8H3lbjoC0FIwGSF/bin/python
 py_require()
 TRUE

──────────────────────────────────────────────────────────────────────────────

References

Girolami, Mark, and Ben Calderhead. 2011. “Riemann Manifold Langevin and Hamiltonian Monte Carlo Methods.” Journal of the Royal Statistical Society: Series B (Statistical Methodology) 73 (2): 123–214. https://doi.org/10.1111/j.1467-9868.2010.00765.x.
Hoffman, Matthew D., and Andrew Gelman. 2014. “The No-U-Turn Sampler: Adaptively Setting Path Lengths in Hamiltonian Monte Carlo.” Journal of Machine Learning Research 15 (47): 1593–623. https://jmlr.org/papers/v15/hoffman14a.html.
Magnusson, Måns, Jakob Torgander, Paul-Christian Bürkner, Lu Zhang, Bob Carpenter, and Aki Vehtari. 2025. “Posteriordb: Testing, Benchmarking and Developing Bayesian Inference Algorithms.” Proceedings of the 28th International Conference on Artificial Intelligence and Statistics (AISTATS). https://arxiv.org/abs/2407.04967.
Vehtari, Aki, Andrew Gelman, Daniel Simpson, Bob Carpenter, and Paul-Christian Bürkner. 2021. “Rank-Normalization, Folding, and Localization: An Improved R̂ for Assessing Convergence of MCMC (with Discussion).” Bayesian Analysis 16 (2): 667–718. https://doi.org/10.1214/20-BA1221.