Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/scripts/rstan-fit-check.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Compile and fit a trivial Stan model. Building rstan proves the headers and
# link line are right; actually running a model additionally exercises the TBB
# runtime that RcppParallel loaded -- including the reduce_sum / task isolation
# path that the old Rtools42 TBB could not support.

library(rstan)

code <- "
data {
int<lower=0> N;
vector[N] y;
}
parameters {
real mu;
}
model {
y ~ normal(mu, 1);
}
"

set.seed(42)
data <- list(N = 20L, y = rnorm(20, mean = 3))

fit <- stan(
model_code = code,
data = data,
chains = 1L,
iter = 200L,
refresh = 0L
)

estimate <- mean(rstan::extract(fit, "mu")[["mu"]])
writeLines(sprintf("posterior mean of mu: %.3f (data mean %.3f)", estimate, mean(data$y)))

# a wildly wrong answer would mean the model ran but the sampler is broken;
# with 20 observations and unit scale this is a very loose bound
if (!is.finite(estimate) || abs(estimate - mean(data$y)) > 1)
stop("posterior mean is implausible; the fit did not work correctly")

writeLines("** rstan fit check passed")
106 changes: 106 additions & 0 deletions .github/scripts/tbb-downstream-check.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Build and load a translation unit exercising the parts of the TBB API that
# downstream packages depend on, using only the flags RcppParallel advertises.
#
# StanHeaders -- and so rstan -- needs both of the pieces used here:
# tbb::this_task_arena::isolate, to keep its thread-local AD tape from being
# modified by a task from another arena, and tbb::task_scheduler_observer, to
# install that tape on each worker. Rtools42's TBB 2017 provides neither in a
# usable form, which is what broke rstan on R 4.2 (isolate is gated behind
# TBB_PREVIEW_TASK_ISOLATION, and isolate_within_arena isn't in the library at
# all), so this stands in for a full rstan build.

code <- '
#include <tbb/blocked_range.h>
#include <tbb/parallel_for.h>
#include <tbb/task_arena.h>
#include <tbb/task_scheduler_observer.h>

#include <atomic>
#include <cstddef>

// R.h remaps names like \'length\' onto Rf_ equivalents by default, which
// collides with the standard library; keep it last, and unremapped
#define R_NO_REMAP
#include <R.h>
#include <Rinternals.h>

namespace {

// mirrors StanHeaders\' ad_tape_observer, which is what pulls the
// task_scheduler_observer entry points into the link
struct observer : public tbb::task_scheduler_observer {
observer() : tbb::task_scheduler_observer() { observe(true); }
void on_scheduler_entry(bool) override {}
void on_scheduler_exit(bool) override {}
};

} // end anonymous namespace

extern "C" SEXP tbb_downstream_check(void) {

observer obs;
std::atomic<int> total(0);

// mirrors StanHeaders\' use of task isolation in reduce_sum / map_rect
tbb::this_task_arena::isolate([&] {
tbb::parallel_for(
tbb::blocked_range<std::size_t>(0, 1024),
[&](const tbb::blocked_range<std::size_t>& range) {
total += static_cast<int>(range.end() - range.begin());
}
);
});

return Rf_ScalarInteger(total.load());

}
'

# report how RcppParallel is configured, so a failure here can be read
# against the provenance logged during installation
writeLines(c(
sprintf("TBB_ENABLED : %s", RcppParallel:::TBB_ENABLED),
sprintf("TBB_LIB : '%s'", RcppParallel:::TBB_LIB),
sprintf("TBB_INC : '%s'", RcppParallel:::TBB_INC),
sprintf("CxxFlags() : %s", RcppParallel:::tbbCxxFlags()),
sprintf("LdFlags() : %s", RcppParallel:::tbbLdFlags())
))

if (!RcppParallel:::TBB_ENABLED)
stop("RcppParallel was installed without a tbb backend")

# mirror rstan's src/Makevars.win, which takes all of its TBB configuration
# from RcppParallel: its compiler flags via CxxFlags(), its linker flags via
# RcppParallelLibs(), and the include path itself via 'LinkingTo'
makevars <- c(
"CXX_STD = CXX17",
sprintf("PKG_CPPFLAGS = -I\"%s\"", system.file("include", package = "RcppParallel")),
"PKG_CPPFLAGS += $(shell \"${R_HOME}/bin/Rscript\" -e \"RcppParallel::CxxFlags()\" | tail -n 1)",
"PKG_LIBS += $(shell \"${R_HOME}/bin${R_ARCH_BIN}/Rscript\" -e \"RcppParallel::RcppParallelLibs()\" | tail -n 1)"
)

dir <- tempfile("tbb-downstream-")
dir.create(dir, recursive = TRUE)
owd <- setwd(dir)
on.exit(setwd(owd), add = TRUE)

writeLines(code, "check.cpp")

# deliberately not named 'Makevars': R CMD SHLIB reads a Makevars in the
# working directory as well as R_MAKEVARS_USER, and would apply both. going
# through R_MAKEVARS_USER also keeps any personal ~/.R/Makevars out of the way
writeLines(makevars, "makevars-downstream")
Sys.setenv(R_MAKEVARS_USER = file.path(dir, "makevars-downstream"))
status <- system(paste(shQuote(file.path(R.home("bin"), "R")), "CMD SHLIB check.cpp"))
if (status != 0L)
stop("downstream translation unit failed to build")

# loading also proves any load-time dependency on the tbb stub resolves
dll <- dyn.load(paste0("check", .Platform$dynlib.ext))
on.exit(dyn.unload(dll[["path"]]), add = TRUE)

result <- .Call("tbb_downstream_check")
if (!identical(result, 1024L))
stop("downstream check returned ", result, "; expected 1024")

writeLines("** downstream TBB check passed")
42 changes: 42 additions & 0 deletions .github/workflows/R-CMD-check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ jobs:
- {os: ubuntu-24.04-arm, r: 'release'}
- {os: windows-latest, r: 'release'}
- {os: windows-11-arm, r: 'release'}
# Rtools42 provides only a pre-oneTBB copy of TBB, so this is the
# one configuration that builds the bundled oneTBB on Windows
- {os: windows-latest, r: '4.2'}

env:
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
Expand All @@ -32,6 +35,7 @@ jobs:

- uses: r-lib/actions/setup-r@v2
with:
r-version: ${{ matrix.config.r }}
use-public-rspm: true

- uses: r-lib/actions/setup-r-dependencies@v2
Expand All @@ -40,3 +44,41 @@ jobs:
needs: check

- uses: r-lib/actions/check-r-package@v2

# Verify that a package can still compile and link against the TBB API that
# RcppParallel advertises. This is the contract rstan (via StanHeaders)
# depends on, and it is not covered by R CMD check: RcppParallel can install
# perfectly well while leaving downstream packages unable to build.
downstream:
runs-on: windows-latest

name: downstream (${{ matrix.r }})

strategy:
fail-fast: false
matrix:
r: ['4.2', 'release']

env:
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
VERBOSE: 1

# the default shell on Windows runners is PowerShell, which aliases 'r'
# to Invoke-History and so mangles 'R CMD INSTALL'
defaults:
run:
shell: bash

steps:
- uses: actions/checkout@v3

- uses: r-lib/actions/setup-r@v2
with:
r-version: ${{ matrix.r }}
use-public-rspm: true

- name: install RcppParallel
run: R CMD INSTALL --preclean .

- name: build against RcppParallel
run: Rscript .github/scripts/tbb-downstream-check.R
96 changes: 96 additions & 0 deletions .github/workflows/rstan.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Build rstan from source against this branch's RcppParallel.
#
# The 'downstream' job in R-CMD-check.yaml is a stand-in for this: it compiles a
# small translation unit using the two TBB APIs StanHeaders needs. That catches
# the breakage this workflow exists to rule out, in seconds rather than the best
# part of an hour -- so this is deliberately manual, for when the real thing is
# worth waiting for (before a release, or after touching the Windows TBB setup).
on:
workflow_dispatch:
inputs:
r-version:
description: "R version (4.2 uses Rtools42, which has no oneTBB)"
required: true
default: "4.2"
os:
description: "Runner to build on"
required: true
default: "windows-latest"
type: choice
options:
- windows-latest
- macOS-latest
- ubuntu-latest
fit-model:
description: "Also compile and fit a tiny Stan model (slow)"
required: false
default: false
type: boolean

name: rstan

jobs:
rstan:
runs-on: ${{ inputs.os }}

name: rstan (${{ inputs.os }}, R ${{ inputs.r-version }})

# building rstan from source is slow, and slower still on Windows
timeout-minutes: 120

env:
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
VERBOSE: 1
# rstan's own guidance for building it from source
MAKEFLAGS: -j2

defaults:
run:
shell: bash

steps:
- uses: actions/checkout@v3

- uses: r-lib/actions/setup-r@v2
with:
r-version: ${{ inputs.r-version }}
use-public-rspm: true

- name: install RcppParallel from this branch
run: R CMD INSTALL --preclean .

- name: report how RcppParallel resolved TBB
run: |
Rscript -e 'writeLines(c(
paste("TBB_ENABLED", RcppParallel:::TBB_ENABLED),
paste("TBB_LIB ", RcppParallel:::TBB_LIB),
paste("CxxFlags ", RcppParallel:::tbbCxxFlags()),
paste("LdFlags ", RcppParallel:::tbbLdFlags())
))'

# binaries are fine for everything except rstan and StanHeaders, which are
# the packages that actually consume RcppParallel's flags. this is
# rstan's Imports plus its LinkingTo, minus RcppParallel (installed above)
# and StanHeaders (built from source below)
- name: install rstan's dependencies
run: |
Rscript -e 'install.packages(c("Rcpp","RcppEigen","BH","inline","gridExtra","loo","pkgbuild","QuickJSR","ggplot2"))'

# dependencies = FALSE so that resolving StanHeaders/rstan can't quietly
# pull CRAN's RcppParallel over the one we just built -- that would turn
# this into a test of the released package instead
- name: install StanHeaders and rstan from source
run: |
Rscript -e 'install.packages(c("StanHeaders","rstan"), type = "source", dependencies = FALSE, INSTALL_opts = "--no-multiarch")'

- name: check RcppParallel is still the one we built
run: |
Rscript -e 'v <- as.character(packageVersion("RcppParallel")); writeLines(paste("RcppParallel", v)); if (!grepl("[.]9000$", v)) stop("RcppParallel was replaced by a released build; the rstan build did not test this branch")'

- name: load rstan
run: |
Rscript -e 'library(rstan); print(rstan::stan_version())'

- name: compile and fit a tiny model
if: ${{ inputs.fit-model }}
run: Rscript .github/scripts/rstan-fit-check.R
19 changes: 19 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,25 @@
'random_access_iterator' in namespace 'std'". This affected CRAN's macOS
x86_64 machines, which pair Apple clang 14 with the macOS 11.3 SDK.

* On Windows, RcppParallel now only uses the copy of TBB provided by Rtools
when that copy is oneTBB; otherwise, the bundled oneTBB is built from
sources, as it already is on other platforms. Rtools42 provides Intel TBB
2017, whose headers downstream packages cannot build against: StanHeaders
uses `tbb::this_task_arena::isolate`, which that release still gates behind
`TBB_PREVIEW_TASK_ISOLATION` and does not export from its library. As a
result, rstan could no longer be built on R 4.2 for Windows.

* On Windows, `RcppParallel::RcppParallelLibs()` once again offers the TBB
stub library, so that packages taking all of their linker flags from it
(e.g. rstan) can resolve TBB symbols which `RcppParallel.dll` does not
itself re-export.

* Fixed an issue on Windows where a failure to build the TBB stub library
could go unreported, since `R CMD SHLIB` has been seen to exit successfully
even when the link failed. Installation completed, but shipped a package
with no `tbb.dll` -- which packages linking `-ltbb` then discovered only
when they failed to load. Installation now fails instead.

* Fixed an issue where building the bundled oneTBB could fail when `CXX`
(or `CC`) was configured with a leading compiler launcher such as `ccache`
(e.g. `CXX = "ccache g++"`). The launcher is now forwarded to cmake via
Expand Down
17 changes: 15 additions & 2 deletions R/tbb.R
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,25 @@ tbbLdFlags <- function() {

# on Windows, we statically link to oneTBB
if (is_windows()) {

libPath <- archSystemFile("libs")

ldFlags <- sprintf("-L%s -lRcppParallel", asBuildPath(libPath))

# also offer the stub library. RcppParallel.dll re-exports whichever TBB
# objects happened to be pulled out of the static library when it was
# linked, which is an incidental export surface rather than a declared
# one; the stub exports the runtime wholesale, so anything missing from
# RcppParallel.dll can still be resolved. this comes last deliberately:
# the linker satisfies symbols in order, so RcppParallel's own exports
# still win, and no dependency on the stub is recorded unless something
# actually needs it
tbbPath <- archSystemFile("lib")
if (file.exists(file.path(tbbPath, "tbb.dll")))
ldFlags <- paste(ldFlags, sprintf("-L%s -ltbb", asBuildPath(tbbPath)))

return(ldFlags)

}

# shortcut if TBB_LIB defined
Expand Down
Loading
Loading