Skip to content

Commit e27a88f

Browse files
authored
Merge pull request #1168 from stan-dev/gq-timing
add generate_quantities timing support
2 parents 24acf13 + 9f9f877 commit e27a88f

12 files changed

Lines changed: 239 additions & 26 deletions

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ set to `TRUE` for an entire R session via new global options. (#1159)
1212
* `cmdstan_model()` no longer fails when `MAKEFLAGS` enables directory-printing
1313
output while reading `STANCFLAGS` from `make`. (#1163)
1414
* `cmdstan_model()` now retains include paths when initialized with both a Stan file and a precompiled executable, fixing model introspection for programs that use `#include` (#1094).
15+
* `$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)
1516
* `laplace()` no longer overwrites the internally generated optimizer CSV when
1617
`mode = NULL` and `output_basename` is supplied. The internally generated
1718
optimizer CSV now uses the filename `<output_basename>-mode-1.csv`.

R/csv.R

Lines changed: 32 additions & 0 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
#'
@@ -456,6 +462,7 @@ read_cmdstan_csv <- function(files,
456462
}
457463
list(
458464
metadata = metadata,
465+
time = list(total = NA_integer_, chains = metadata$time),
459466
generated_quantities = draws
460467
)
461468
} else if (metadata$method == "pathfinder") {
@@ -653,6 +660,21 @@ for (method in unavailable_methods_CmdStanFit_CSV) {
653660

654661
# csv reading internals ---------------------------------------------------
655662

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+
656678
#' Reads the sampling arguments and the diagonal of the
657679
#' inverse mass matrix from the comments in a CSV file.
658680
#'
@@ -786,6 +808,11 @@ read_csv_metadata <- function(csv_file) {
786808
tmp <- gsub("seconds (Total)", "", tmp, fixed = TRUE)
787809
tmp <- trimws(gsub(" Elapsed Time: ", "", tmp, fixed = TRUE))
788810
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+
}
789816
}
790817
if (!is.null(csv_file_info$method) &&
791818
csv_file_info$method == "diagnose" &&
@@ -827,6 +854,11 @@ read_csv_metadata <- function(csv_file) {
827854
sampling = sampling_time,
828855
total = total_time
829856
)
857+
} else if (csv_file_info$method == "generate_quantities") {
858+
csv_file_info$time <- data.frame(
859+
chain_id = csv_file_info$id,
860+
total = total_time
861+
)
830862
}
831863
csv_file_info$model <- NULL
832864
csv_file_info$engaged <- NULL

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: 10 additions & 3 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)
@@ -1143,6 +1143,8 @@ CmdStanGQProcs <- R6::R6Class(
11431143
if (self$is_still_working(id) && !self$is_queued(id) && !self$is_alive(id)) {
11441144
# if the process just finished make sure we process all
11451145
# input and mark the process finished
1146+
self$process_output(id)
1147+
self$process_error_output(id)
11461148
if (self$get_proc(id)$get_exit_status() == 0) {
11471149
self$set_proc_state(id = id, new_state = 5) # mark_proc_stop will mark this process successful
11481150
} else {
@@ -1164,8 +1166,13 @@ CmdStanGQProcs <- R6::R6Class(
11641166
if (nzchar(line)) {
11651167
if (self$proc_state(id) == 1 && grepl("refresh = ", line, perl = TRUE)) {
11661168
self$set_proc_state(id, new_state = 1.5)
1167-
} else if (self$proc_state(id) >= 2 && private$show_stdout_messages_) {
1168-
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+
}
11691176
}
11701177
} else {
11711178
# 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.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# stan_version_major = 2
2+
# stan_version_minor = 39
3+
# stan_version_patch = 0
4+
# model = bernoulli_ppc_model
5+
# method = generate_quantities
6+
# generate_quantities
7+
# fitted_params = /tmp/RtmpCvOIQ1/fittedParams-202006271227-1-b85b52.csv
8+
# id = 1
9+
# data
10+
# file = /home/rok/.cmdstanr/cmdstan-2.23.0/examples/bernoulli/bernoulli.data.json
11+
# init = 2 (Default)
12+
# random
13+
# seed = 123
14+
# output
15+
# file = /tmp/RtmpCvOIQ1/bernoulli_ppc-202006271227-1-986540.csv
16+
# diagnostic_file = (Default)
17+
# refresh = 250
18+
y_rep.1,y_rep.2,y_rep.3,y_rep.4,y_rep.5,y_rep.6,y_rep.7,y_rep.8,y_rep.9,y_rep.10
19+
0,0,0,0,0,0,0,0,0,0
20+
0,1,1,0,1,0,1,1,0,0
21+
0,0,0,0,0,0,0,0,0,0
22+
0,0,0,0,0,1,0,0,0,1
23+
1,0,0,0,0,0,0,0,0,0
24+
#
25+
# Elapsed Time: 0.123 seconds (Generated Quantities)
26+
#
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# stan_version_major = 2
2+
# stan_version_minor = 39
3+
# stan_version_patch = 0
4+
# model = bernoulli_ppc_model
5+
# method = generate_quantities
6+
# generate_quantities
7+
# fitted_params = /tmp/RtmpCvOIQ1/fittedParams-202006271227-2-b85b52.csv
8+
# id = 2
9+
# data
10+
# file = /home/rok/.cmdstanr/cmdstan-2.23.0/examples/bernoulli/bernoulli.data.json
11+
# init = 2 (Default)
12+
# random
13+
# seed = 456
14+
# output
15+
# file = /tmp/RtmpCvOIQ1/bernoulli_ppc-202006271227-2-986540.csv
16+
# diagnostic_file = (Default)
17+
# refresh = 250
18+
y_rep.1,y_rep.2,y_rep.3,y_rep.4,y_rep.5,y_rep.6,y_rep.7,y_rep.8,y_rep.9,y_rep.10
19+
1,0,1,0,0,0,1,0,0,1
20+
0,1,0,0,1,0,0,1,0,0
21+
1,0,0,0,0,1,0,0,0,0
22+
0,0,1,0,0,0,0,1,0,0
23+
0,1,0,0,0,0,0,0,1,0
24+
#
25+
# Elapsed Time: 0.456 seconds (Generated Quantities)
26+
#

tests/testthat/test-csv.R

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,65 @@ test_that("time from read_cmdstan_csv matches time from fit$time()", {
524524
)
525525
})
526526

527+
test_that("returning time works for gq read_cmdstan_csv from static CSV", {
528+
csv_files <- test_path("resources", "csv", "bernoulli_ppc-1-gq-with-timing.csv")
529+
csv_data <- read_cmdstan_csv(csv_files)
530+
expect_equal(csv_data$time$total, NA_integer_)
531+
expect_equal(csv_data$time$chains, data.frame(
532+
chain_id = 1,
533+
total = 0.123
534+
))
535+
536+
csv_files <- c(
537+
test_path("resources", "csv", "bernoulli_ppc-1-gq-with-timing.csv"),
538+
test_path("resources", "csv", "bernoulli_ppc-2-gq-with-timing.csv")
539+
)
540+
csv_data <- read_cmdstan_csv(csv_files)
541+
expect_equal(csv_data$time$total, NA_integer_)
542+
expect_equal(csv_data$time$chains, data.frame(
543+
chain_id = c(1, 2),
544+
total = c(0.123, 0.456)
545+
))
546+
})
547+
548+
test_that("returning time works for gq read_cmdstan_csv from fit object", {
549+
gq_csv <- read_cmdstan_csv(fit_gq$output_files())
550+
expect_equal(gq_csv$time$total, NA_integer_)
551+
checkmate::expect_data_frame(
552+
gq_csv$time$chains,
553+
any.missing = FALSE,
554+
types = c("numeric", "numeric"),
555+
nrows = fit_gq$num_chains(),
556+
ncols = 2
557+
)
558+
expect_named(gq_csv$time$chains, c("chain_id", "total"))
559+
if (cmdstan_version() >= "2.39.0") {
560+
# per-chain times should be non-zero (parsed from CmdStan timing output)
561+
expect_gt(min(gq_csv$time$chains$total), 0)
562+
} else {
563+
# for version < 2.39 per-chain times are reported as 0
564+
expect_equal(gq_csv$time$chains$total, rep(0, fit_gq$num_chains()))
565+
}
566+
})
567+
568+
test_that("gq time from read_cmdstan_csv matches time from fit_gq$time()", {
569+
expect_equal(
570+
read_cmdstan_csv(fit_gq$output_files())$time$chains,
571+
fit_gq$time()$chains,
572+
ignore_attr = TRUE
573+
)
574+
})
575+
576+
test_that("gq CSV without timing returns zero chain time", {
577+
csv_files <- test_path("resources", "csv", "bernoulli_ppc-1-gq.csv")
578+
csv_data <- read_cmdstan_csv(csv_files)
579+
expect_equal(csv_data$time$total, NA_integer_)
580+
expect_equal(csv_data$time$chains, data.frame(
581+
chain_id = 1,
582+
total = 0
583+
))
584+
})
585+
527586
test_that("read_cmdstan_csv reads seed correctly", {
528587
opt <- read_cmdstan_csv(fit_bernoulli_optimize$output_files())
529588
vi <- read_cmdstan_csv(fit_bernoulli_variational$output_files())

0 commit comments

Comments
 (0)