Skip to content

Commit 39484af

Browse files
committed
fixes to GQ time handling and documentation
1 parent 669445e commit 39484af

10 files changed

Lines changed: 120 additions & 41 deletions

File tree

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ set to `TRUE` for an entire R session via new global options. (#1159)
1111
* `save_metric_files()` now gives an informative error when metric files were not created and keeps saved metric files after the fitted model is garbage-collected. (#1021)
1212
* `cmdstan_model()` no longer fails when `MAKEFLAGS` enables directory-printing
1313
output while reading `STANCFLAGS` from `make`. (#1163)
14+
* `$generate_quantities()` now reports per-process execution times with CmdStan 2.39 or newer, and `read_cmdstan_csv()` returns these times from standalone generated quantities CSV files. (#1168)
1415
* `laplace()` no longer overwrites the internally generated optimizer CSV when
1516
`mode = NULL` and `output_basename` is supplied. The internally generated
1617
optimizer CSV now uses the filename `<output_basename>-mode-1.csv`.

R/csv.R

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@
7878
#' For [standalone generated quantities][model-method-generate-quantities] the
7979
#' returned list also includes the following components:
8080
#'
81+
#' * `time`: Run time information for the individual processes, with one row in
82+
#' the `chains` data frame per CSV file. The returned object is the same as for
83+
#' the [$time()][fit-method-time] method except the total run time can't be
84+
#' inferred directly from the CSV files (they may have been generated in
85+
#' parallel) and is therefore `NA`. For CmdStan versions before 2.39 the
86+
#' individual process times are reported as zero.
8187
#' * `generated_quantities`: A [`draws_array`][posterior::draws_array] of
8288
#' the generated quantities.
8389
#'
@@ -654,6 +660,21 @@ for (method in unavailable_methods_CmdStanFit_CSV) {
654660

655661
# csv reading internals ---------------------------------------------------
656662

663+
parse_generated_quantities_time <- function(line) {
664+
suffix <- "seconds (Generated Quantities)"
665+
line <- trimws(line)
666+
if (!startsWith(line, "Elapsed Time:") || !endsWith(line, suffix)) {
667+
return(NULL)
668+
}
669+
time <- trimws(sub(suffix, "", line, fixed = TRUE))
670+
time <- trimws(sub("Elapsed Time:", "", time, fixed = TRUE))
671+
time <- suppressWarnings(as.double(time))
672+
if (is.na(time)) {
673+
return(NULL)
674+
}
675+
time
676+
}
677+
657678
#' Reads the sampling arguments and the diagonal of the
658679
#' inverse mass matrix from the comments in a CSV file.
659680
#'
@@ -787,10 +808,11 @@ read_csv_metadata <- function(csv_file) {
787808
tmp <- gsub("seconds (Total)", "", tmp, fixed = TRUE)
788809
tmp <- trimws(gsub(" Elapsed Time: ", "", tmp, fixed = TRUE))
789810
total_time <- as.numeric(tmp)
790-
} else if (grepl("(Generated Quantities)", tmp, fixed = TRUE)) {
791-
tmp <- gsub("seconds (Generated Quantities)", "", tmp, fixed = TRUE)
792-
tmp <- trimws(gsub("Elapsed Time:", "", tmp, fixed = TRUE))
793-
total_time <- as.numeric(tmp)
811+
} else {
812+
generated_quantities_time <- parse_generated_quantities_time(tmp)
813+
if (!is.null(generated_quantities_time)) {
814+
total_time <- generated_quantities_time
815+
}
794816
}
795817
if (!is.null(csv_file_info$method) &&
796818
csv_file_info$method == "diagnose" &&

R/fit.R

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,19 +1090,26 @@ CmdStanFit$set("public", name = "metric_files", value = metric_files)
10901090
#'
10911091
#' @name fit-method-time
10921092
#' @aliases time
1093-
#' @description Report the run time in seconds. For MCMC additional information
1094-
#' is provided about the run times of individual chains and the warmup and
1095-
#' sampling phases. For Laplace approximation the time only include the time
1096-
#' for drawing the approximate sample and does not include the time
1097-
#' taken to run the `$optimize()` method.
1093+
#' @description Report the run time in seconds. For MCMC and standalone
1094+
#' generated quantities additional information is provided about the run
1095+
#' times of individual chains or processes. For MCMC, timing information is
1096+
#' also provided for the warmup and sampling phases. For Laplace approximation
1097+
#' the time only includes the time for drawing the approximate sample and does
1098+
#' not include the time taken to run the `$optimize()` method.
10981099
#'
10991100
#' @return
11001101
#' A list with elements
1101-
#' * `total`: (scalar) The total run time. For MCMC this may be different than
1102-
#' the sum of the chain run times if parallelization was used.
1103-
#' * `chains`: (data frame) For MCMC only, timing info for the individual
1104-
#' chains. The data frame has columns `"chain_id"`, `"warmup"`, `"sampling"`,
1105-
#' and `"total"`.
1102+
#' * `total`: (scalar) The total run time. For MCMC and standalone generated
1103+
#' quantities this may differ from the sum of the individual run times if
1104+
#' parallelization was used.
1105+
#' * `chains`: (data frame) For MCMC and standalone generated quantities,
1106+
#' timing information for the individual chains. For MCMC the data frame has
1107+
#' columns `"chain_id"`, `"warmup"`, `"sampling"`, and `"total"`. For standalone
1108+
#' generated quantities, each row corresponds to one fitted-parameter CSV file
1109+
#' and one CmdStan process, and the data frame has columns `"chain_id"` and
1110+
#' `"total"`. Variational or optimization input therefore produces one row.
1111+
#' With CmdStan versions before 2.39, standalone generated quantities process
1112+
#' times are reported as zero.
11061113
#'
11071114
#' @seealso [`CmdStanMCMC`], [`CmdStanMLE`], [`CmdStanVB`], [`CmdStanGQ`]
11081115
#'
@@ -2250,7 +2257,7 @@ CmdStanPathfinder$set("public", name = "lp_approx", value = lp_approx)
22502257
#'
22512258
#' |**Method**|**Description**|
22522259
#' |:----------|:---------------|
2253-
#' [`$time()`][fit-method-time] | Report the total run time. |
2260+
#' [`$time()`][fit-method-time] | Report total and process-specific run times. |
22542261
#' [`$output()`][fit-method-output] | Return the stdout and stderr of all chains or pretty print the output for a single chain. |
22552262
#' [`$return_codes()`][fit-method-return_codes] | Return the return codes from the CmdStan runs. |
22562263
#'

R/run.R

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ CmdStanRun <- R6::R6Class(
357357
} else if (self$method() == "generate_quantities") {
358358
chain_time <- data.frame(
359359
chain_id = self$procs$proc_ids()[self$procs$is_finished()],
360-
total = self$procs$proc_total_time()[self$procs$is_finished()]
360+
total = as.vector(self$procs$proc_total_time())
361361
)
362362

363363
time <- list(total = self$procs$total_time(), chains = chain_time)
@@ -1166,10 +1166,13 @@ CmdStanGQProcs <- R6::R6Class(
11661166
if (nzchar(line)) {
11671167
if (self$proc_state(id) == 1 && grepl("refresh = ", line, perl = TRUE)) {
11681168
self$set_proc_state(id, new_state = 1.5)
1169-
} else if (grepl("Elapsed Time:", line, fixed = TRUE)) {
1170-
private$proc_total_time_[[id]] <- as.double(trimws(sub("Elapsed Time:", "", sub("seconds (Generated Quantities)", "", line, fixed = TRUE), fixed = TRUE)))
1171-
} else if (self$proc_state(id) >= 2 && private$show_stdout_messages_) {
1172-
cat("Chain", id, line, "\n")
1169+
} else {
1170+
generated_quantities_time <- parse_generated_quantities_time(line)
1171+
if (!is.null(generated_quantities_time)) {
1172+
private$proc_total_time_[[id]] <- generated_quantities_time
1173+
} else if (self$proc_state(id) >= 2 && private$show_stdout_messages_) {
1174+
cat("Chain", id, line, "\n")
1175+
}
11731176
}
11741177
} else {
11751178
# after the metadata is printed and we found a blank line

man/CmdStanGQ.Rd

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/fit-method-time.Rd

Lines changed: 17 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/read_cmdstan_csv.Rd

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test-csv.R

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -558,21 +558,22 @@ test_that("returning time works for gq read_cmdstan_csv from fit object", {
558558
expect_named(gq_csv$time$chains, c("chain_id", "total"))
559559
if (cmdstan_version() >= "2.39.0") {
560560
# per-chain times should be non-zero (parsed from CmdStan timing output)
561-
expect_true(all(gq_csv$time$chains$total > 0))
561+
expect_gt(min(gq_csv$time$chains$total), 0)
562562
} else {
563563
# for version < 2.39 per-chain times are reported as 0
564-
expect_true(all(gq_csv$time$chains$total == 0))
564+
expect_equal(gq_csv$time$chains$total, rep(0, fit_gq$num_chains()))
565565
}
566566
})
567567

568568
test_that("gq time from read_cmdstan_csv matches time from fit_gq$time()", {
569-
expect_equivalent(
569+
expect_equal(
570570
read_cmdstan_csv(fit_gq$output_files())$time$chains,
571-
fit_gq$time()$chains
571+
fit_gq$time()$chains,
572+
ignore_attr = TRUE
572573
)
573574
})
574575

575-
test_that("returning time is NULL for gq CSV without timing", {
576+
test_that("gq CSV without timing returns zero chain time", {
576577
csv_files <- test_path("resources", "csv", "bernoulli_ppc-1-gq.csv")
577578
csv_data <- read_cmdstan_csv(csv_files)
578579
expect_equal(csv_data$time$total, NA_integer_)

tests/testthat/test-fit-gq.R

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,17 +136,46 @@ test_that("time() works after gq", {
136136
ncols = 2
137137
)
138138
expect_named(run_times$chains, c("chain_id", "total"))
139-
# total wall-clock time is always positive
140-
expect_true(run_times$total > 0)
139+
# CmdStanR measures total wall-clock time for every CmdStan version.
140+
expect_gt(run_times$total, 0)
141141
if (cmdstan_version() >= "2.39.0") {
142142
# CmdStan >= 2.39 reports per-chain timing for generated quantities
143-
expect_true(all(run_times$chains$total > 0))
143+
expect_gt(min(run_times$chains$total), 0)
144144
} else {
145145
# for CmdStan < 2.39 per-chain times are reported as 0
146-
expect_true(all(run_times$chains$total == 0))
146+
expect_equal(run_times$chains$total, rep(0, fit_gq$num_chains()))
147147
}
148148
})
149149

150+
test_that("time() aligns successful gq process ids and times", {
151+
# Reproduce a partial failure without running noisy CmdStan subprocesses.
152+
# The first process fails so the successful process ids and times can be
153+
# checked for the misalignment caused by filtering the times twice.
154+
MockGQProcs <- R6::R6Class(
155+
classname = "MockGQProcs",
156+
inherit = CmdStanGQProcs,
157+
public = list(
158+
initialize = function() {
159+
super$initialize(num_procs = 3)
160+
private$proc_state_[] <- c(7, 6, 6)
161+
private$proc_total_time_[] <- c(0, 0.1, 0.2)
162+
private$total_time_ <- 0.3
163+
}
164+
)
165+
)
166+
procs <- fit_gq$runset$procs
167+
on.exit(fit_gq$runset$procs <- procs)
168+
fit_gq$runset$procs <- MockGQProcs$new()
169+
170+
expect_equal(
171+
fit_gq$runset$time(),
172+
list(
173+
total = 0.3,
174+
chains = data.frame(chain_id = c(2L, 3L), total = c(0.1, 0.2))
175+
)
176+
)
177+
})
178+
150179
test_that("fitted_params_files() works", {
151180
expect_equal(
152181
fit_gq$fitted_params_files(),

tests/testthat/test-model-generate_quantities.R

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,11 @@ test_that("generate_quantities works with draws_array", {
7777
test_that("generate_quantities works with VB and draws_matrix", {
7878
fit <- testing_fit("bernoulli", method = "variational", seed = 123)
7979
expect_gq_output(
80-
mod_gq$generate_quantities(data = data_list, fitted_params = fit)
80+
fit_gq <- mod_gq$generate_quantities(data = data_list, fitted_params = fit)
8181
)
82+
run_times <- fit_gq$time()
83+
expect_equal(run_times$chains$chain_id, 1)
84+
expect_gte(run_times$chains$total, 0)
8285
expect_gq_output(
8386
mod_gq$generate_quantities(data = data_list, fitted_params = fit$draws())
8487
)

0 commit comments

Comments
 (0)