Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Handling of cpp_options #1022

Open
wants to merge 30 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
292ed26
Fix opencl tests
katrinabrock Aug 8, 2024
5b8808d
Add testcases for Issue #765
katrinabrock Aug 5, 2024
94250da
Fix Issue #765
katrinabrock Aug 8, 2024
11f0a18
Handle stan_threads cpp option consistent with cmdstan
katrinabrock Aug 21, 2024
1914a42
Improve Docs and Warnings around cpp_options
katrinabrock Aug 21, 2024
a728fb2
WIP
katrinabrock Aug 23, 2024
c9d8532
Re-write cpp arg handling code
katrinabrock Aug 28, 2024
a5572da
fixup remove browser call
katrinabrock Aug 28, 2024
a44052d
fixup: with changes, make opencl tests pass
katrinabrock Aug 28, 2024
a0d2cb0
WIP
katrinabrock Sep 9, 2024
ce56ef8
wip
katrinabrock Sep 9, 2024
ff317b3
WIP
katrinabrock Sep 10, 2024
13483c0
Tests Passing?
katrinabrock Sep 10, 2024
44b7ec8
squash some bugs
katrinabrock Sep 11, 2024
c03b2a0
...how about now?
katrinabrock Sep 11, 2024
b21bc17
fixup
katrinabrock Sep 11, 2024
8fcb784
fixup
katrinabrock Sep 11, 2024
efc90ec
fixup
katrinabrock Sep 11, 2024
da32ce7
defaults list()-> NULL
katrinabrock Dec 17, 2024
dcaed82
remove/rename exe_info variable
katrinabrock Dec 17, 2024
c151100
Update R/model.R
katrinabrock Dec 17, 2024
b002425
Update R/model.R
katrinabrock Dec 17, 2024
73f3bee
add tests of user_header prescidence
katrinabrock Dec 17, 2024
df1a131
fix typo
katrinabrock Dec 17, 2024
9e9e836
add verbose arg to info call
katrinabrock Dec 17, 2024
d1da6bc
Add commend detailing mock logic
katrinabrock Dec 17, 2024
ee2c0fa
move exe file path resolution to separate function
katrinabrock Dec 18, 2024
2f07e3e
remove files removed in master
katrinabrock Dec 18, 2024
dd804d1
lint
katrinabrock Dec 18, 2024
eb3bbf9
fix incomplete method renaming
katrinabrock Dec 19, 2024
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
6 changes: 0 additions & 6 deletions R/args.R
Original file line number Diff line number Diff line change
Expand Up @@ -715,12 +715,6 @@ validate_cmdstan_args <- function(self) {
}
validate_init(self$init, num_inits)
validate_seed(self$seed, num_procs)
if (!is.null(self$opencl_ids)) {
if (cmdstan_version() < "2.26") {
stop("Runtime selection of OpenCL devices is only supported with CmdStan version 2.26 or newer.", call. = FALSE)
}
checkmate::assert_vector(self$opencl_ids, len = 2)
}
invisible(TRUE)
}

Expand Down
486 changes: 387 additions & 99 deletions R/model.R

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion R/path.R
Original file line number Diff line number Diff line change
Expand Up @@ -234,8 +234,16 @@ unset_cmdstan_path <- function() {
}

# fake a cmdstan version (only used in tests)
fake_cmdstan_version <- function(version) {
fake_cmdstan_version <- function(version, mod = NULL) {
.cmdstanr$VERSION <- version
if (!is.null(mod)) {
if (!is.null(mod$.__enclos_env__$private$exe_info_)) {
mod$.__enclos_env__$private$exe_info_$stan_version <- version
}
if (!is.null(mod$.__enclos_env__$private$cmdstan_version_)) {
mod$.__enclos_env__$private$cmdstan_version_ <- version
}
}
}
reset_cmdstan_version <- function() {
.cmdstanr$VERSION <- read_cmdstan_version(cmdstan_path())
Expand Down
7 changes: 4 additions & 3 deletions man/model-method-compile.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions tests/testthat/helper-custom-expectations.R
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,11 @@ expect_noninteractive_silent <- function(object) {
rlang::with_interactive(value = FALSE,
expect_silent(object))
}

expect_equal_ignore_order <- function(object, expected, ...) {
object <- expected[sort(names(object))]
expected <- expected[sort(names(expected))]
expect_equal(object, expected, ...)
}

expect_not_true <- function(...) expect_false(isTRUE(...))
54 changes: 54 additions & 0 deletions tests/testthat/helper-mock-cli.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
real_wcr <- wsl_compatible_run

with_mocked_cli <- function(code, compile_ret, info_ret) {
with_mocked_bindings(
code,
wsl_compatible_run = function(command, args, ...) {
if (
!is.null(command)
&& command == "make"
&& !is.null(args)
&& startsWith(basename(args[1]), "model-")
) {
message("mock-compile-was-called")
compile_ret
} else if (!is.null(args) && args[1] == "info") {
info_ret
} else {
real_wcr(command = command, args = args, ...)
}
}
)
}

######## Mock Compile Expectations #######

# These helpers mimic `assert_called` and `assert_not_called` in other languages.
#
# Logic
# `expect_mock_compile`
# passes if mock_compile is called (at all, doesn't matter how many times)
# fails if mock_compile is never called
# `expect_no_mock_compile` is the inverse. It
# passes if mock_compile is *not* called at all
# fails if mock_compile is called (even once)
#
# Implementation:
# `with_mocked_cli`
# if a compile is triggered
# emits a message with the contents `mock-compile-was-called`
# (defined as wsl_compatible_run being called with make model-*)
# `expect_mock_compile` checks for this message:
# passes if it detects such a message
# fails if it does not
# `expect_no_mock_compile`
# fails if a message with exactly this text is detected
# passes if no such message is detected
# messages with any other text does not impact `expect_no_mock_compile`

expect_mock_compile <- function(object, ...) {
expect_message(object, regexp = "mock-compile-was-called", ...)
}
expect_no_mock_compile <- function(object, ...) {
expect_no_message(object, message = "mock-compile-was-called", ...)
}
5 changes: 5 additions & 0 deletions tests/testthat/helper-models.R
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ cmdstan_example_file <- function() {
file.path(cmdstan_path(), "examples", "bernoulli", "bernoulli.stan")
}

cmdstan_example_exe_file <- function() {
# stan program in different directory from the others
file.path(cmdstan_path(), "examples", "bernoulli", "bernoulli.stan")
}

testing_model <- function(name) {
cmdstan_model(stan_file = testing_stan_file(name))
}
Expand Down
2 changes: 1 addition & 1 deletion tests/testthat/test-example.R
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
context("cmdstanr_example")

test_that("cmdstanr_example works", {
fit_mcmc <- cmdstanr_example("logistic", chains = 2)
fit_mcmc <- cmdstanr_example("logistic", chains = 2, force_recompile = TRUE)
checkmate::expect_r6(fit_mcmc, "CmdStanMCMC")
expect_equal(fit_mcmc$num_chains(), 2)

Expand Down
207 changes: 207 additions & 0 deletions tests/testthat/test-model-compile-user_header.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@

file_that_exists <- "placeholder_exists"
file_that_doesnt_exist <- "placeholder_doesnt_exist"
file.create(file_that_exists)
on.exit(
if (file.exists(file_that_exists)) file.remove(file_that_exists),
add = TRUE,
after = FALSE
)

make_local_orig <- cmdstan_make_local()
cmdstan_make_local(cpp_options = list("PRECOMPILED_HEADERS" = "false"))
on.exit(
cmdstan_make_local(cpp_options = make_local_orig, append = FALSE),
add = TRUE,
after = FALSE
)
hpp <- "
#include <stan/math.hpp>
#include <boost/math/tools/promotion.hpp>
#include <ostream>

namespace bernoulli_external_model_namespace
{
template <typename T0__,
stan::require_all_t<stan::is_stan_scalar<T0__>>* = nullptr>
inline typename boost::math::tools::promote_args<T0__>::type make_odds(
const T0__ & theta,
std::ostream *pstream__
)
{
return theta / (1 - theta);
}
}"

test_that("cmdstan_model works with user_header with mock", {
skip_if(os_is_macos())
tmpfile <- tempfile(fileext = ".hpp")
cat(hpp, file = tmpfile, sep = "\n")

with_mocked_cli(
compile_ret = list(status = 0),
info_ret = list(),
code = expect_mock_compile(
expect_warning(
expect_no_warning({
mod <- cmdstan_model(
stan_file = testing_stan_file("bernoulli_external"),
exe_file = file_that_exists,
user_header = tmpfile
)
}, message = "Recompiling is recommended"),
# ^ this warning should not occur because recompile happens automatically
"Retrieving exe_file info failed"
# ^ this warning should occur
)
)
)

with_mocked_cli(
compile_ret = list(status = 0),
info_ret = list(),
code = expect_mock_compile({
mod_2 <- cmdstan_model(
stan_file = testing_stan_file("bernoulli_external"),
exe_file = file_that_doesnt_exist,
cpp_options = list(USER_HEADER = tmpfile),
stanc_options = list("allow-undefined")
)
})
)

# Check recompilation upon changing header
file.create(file_that_exists)
with_mocked_cli(
compile_ret = list(status = 0),
info_ret = list(),
code = expect_no_mock_compile({
mod$compile(quiet = TRUE, user_header = tmpfile)
})
)

Sys.setFileTime(tmpfile, Sys.time() + 1) # touch file to trigger recompile
with_mocked_cli(
compile_ret = list(status = 0),
info_ret = list(),
code = expect_mock_compile({
mod$compile(quiet = TRUE, user_header = tmpfile)
})
)

# mock does not automatically update file mtime
Sys.setFileTime(mod$exe_file(), Sys.time() + 1) # touch file to trigger recompile

# Alternative spec of user header
with_mocked_cli(
compile_ret = list(status = 0),
info_ret = list(),
code = expect_no_mock_compile({
mod$compile(
quiet = TRUE,
cpp_options = list(user_header = tmpfile),
dry_run = TRUE
)
})
)

# Error/warning messages
with_mocked_cli(
compile_ret = list(status = 1),
info_ret = list(),
code = expect_error(
cmdstan_model(
stan_file = testing_stan_file("bernoulli_external"),
cpp_options = list(USER_HEADER = "non_existent.hpp"),
stanc_options = list("allow-undefined")
),
"header file '[^']*' does not exist"
)
)

with_mocked_cli(
compile_ret = list(status = 1),
info_ret = list(),
code = expect_warning(
cmdstan_model(
stan_file = testing_stan_file("bernoulli_external"),
cpp_options = list(USER_HEADER = tmpfile, user_header = tmpfile),
dry_run = TRUE
),
"User header specified both"
)
)
with_mocked_cli(
compile_ret = list(status = 1),
info_ret = list(),
code = expect_warning(
cmdstan_model(
stan_file = testing_stan_file("bernoulli_external"),
user_header = tmpfile,
cpp_options = list(USER_HEADER = tmpfile),
dry_run = TRUE
),
"User header specified both"
)
)
})

test_that("user_header precedence order is correct", {

tmp_files <- lapply(1:3, function(n) tempfile(fileext = ".hpp"))
lapply(tmp_files, function(filename) cat(hpp, file = filename, sep = "\n"))
on.exit(
{lapply(tmp_files, function(filename) file.remove(filename))},
add = TRUE
)

with_mocked_cli(
compile_ret = list(status = 1),
info_ret = list(),
code = expect_warning({
mod <- cmdstan_model(
stan_file = testing_stan_file("bernoulli_external"),
user_header = tmp_files[[1]],
cpp_options = list(
USER_HEADER = tmp_files[[2]],
user_header = tmp_files[[3]]
),
dry_run = TRUE
)
}, "User header specified both")
)
expect_equal(mod$precompile_cpp_options()$user_header, tmp_files[[1]])

with_mocked_cli(
compile_ret = list(status = 1),
info_ret = list(),
code = expect_warning({
mod <- cmdstan_model(
stan_file = testing_stan_file("bernoulli_external"),
cpp_options = list(
USER_HEADER = tmp_files[[2]],
user_header = tmp_files[[3]]
),
dry_run = TRUE
)
}, "User header specified both")
)
expect_equal(mod$precompile_cpp_options()$user_header, tmp_files[[2]])

with_mocked_cli(
compile_ret = list(status = 1),
info_ret = list(),
code = expect_warning({
mod <- cmdstan_model(
stan_file = testing_stan_file("bernoulli_external"),
cpp_options = list(
user_header = tmp_files[[3]],
USER_HEADER = tmp_files[[2]]
),
dry_run = TRUE
)
}, "User header specified both")
)
expect_equal(mod$precompile_cpp_options()$user_header, tmp_files[[3]])

})
Loading
Loading