Parallelizing functions of the decider package
parallelization_jointBLRM.Rmd
Four of the main functions of the decider package come with a
built-in parallelization using the foreach
package, namely,
the functions:
Leveraging this capability requires the registration of a parallel
bakcend by the user. The present vignette will provide some basic
examples how such a backend can be registered. A similar vignette is
provided by the bhmbasket
package, which inspired the creation of the present vignette.
Simple parallelization
First, the following packages are loaded
With the doFuture
, we register a simple parallel backend
with a given number of cores, set by the argument n.cores
.
For the purpose of this vignette, we will use a single core due to
technical reasons, but when running the code locally, one can easily
change this depending on the number of cores available.
#specify number of cores -- here, only 1 for a sequential execution
n.cores <- 1
#register backend for parallel execution and plan multisession
doFuture::registerDoFuture()
future::plan(future::multisession, workers=n.cores)
To figure out how many cores are available, one can use e.g.
parallel::detectCores()
#> [1] 8
Following the initialization of the parallel backend, you can simply
run one of the four parallelized functions from the decider
package. They will automatically leverage the number of cores
registered.
As an example, a simple simulation could be accomplished by:
sim_result <- sim_jointBLRM(
active.mono1.a = TRUE,
active.combi.a = TRUE,
doses.mono1.a = c(2, 4, 8),
doses.combi.a = rbind(
c(2, 4, 8, 2, 4, 8),
c(1, 1, 1, 2, 2, 2)
),
dose.ref1 = 8,
dose.ref2 = 2,
start.dose.mono1.a = 2,
start.dose.combi.a1 = 2,
start.dose.combi.a2 = 1,
tox.mono1.a = c(0.2, 0.3, 0.5),
tox.combi.a = c(0.3, 0.4, 0.6, 0.4, 0.5, 0.7),
max.n.mono1.a = 12,
max.n.combi.a = 24,
cohort.size = 3,
cohort.queue = c(1, 1, 1, rep(c(1, 3), times = 100)),
n.studies = 3
)
print(sim_result, quote = F)
#> $`results combi.a`
#> underdose target dose overdose max n reached before MTD all doses too toxic
#> Number of trials 0 0 0 0 3
#> Percentage 0 0 0 0 100
#> Percentage not all too toxic
#>
#> $`summary combi.a`
#> Median Mean Min. Max. 2.5% 97.5%
#> #Pat underdose 0.0000000 0.0000000 0.0000000 0 0.00 0.0000000
#> #Pat target dose 3.0000000 3.0000000 3.0000000 3 3.00 3.0000000
#> #Pat overdose 0.0000000 0.0000000 0.0000000 0 0.00 0.0000000
#> #Pat (all) 3.0000000 3.0000000 3.0000000 3 3.00 3.0000000
#> #DLT underdose 0.0000000 0.0000000 0.0000000 0 0.00 0.0000000
#> #DLT target dose 2.0000000 2.0000000 1.0000000 3 1.05 2.9500000
#> #DLT overdose 0.0000000 0.0000000 0.0000000 0 0.00 0.0000000
#> #DLT (all) 2.0000000 2.0000000 1.0000000 3 1.05 2.9500000
#> % overdose 0.0000000 0.0000000 0.0000000 0 0.00 0.0000000
#> % DLT 0.6666667 0.6666667 0.3333333 1 0.35 0.9833333
#>
#> $`MTDs combi.a`
#> 2+1 4+1 8+1 2+2 4+2 8+2
#> Dose 2+1 4+1 8+1 2+2 4+2 8+2
#> True P(DLT) 0.3 0.4 0.6 0.4 0.5 0.7
#> True category target dose overdose overdose overdose overdose overdose
#> MTD declared (n) 0 0 0 0 0 0
#>
#> $`#pat. combi.a`
#> 2+1 4+1 8+1 2+2 4+2 8+2
#> Dose 2+1 4+1 8+1 2+2 4+2 8+2
#> mean #pat 3 0 0 0 0 0
#> median #pat 3 0 0 0 0 0
#> min #pat 3 0 0 0 0 0
#> max #pat 3 0 0 0 0 0
#>
#> $`#DLT combi.a`
#> 2+1 4+1 8+1 2+2 4+2 8+2
#> Dose 2+1 4+1 8+1 2+2 4+2 8+2
#> mean #DLT 2 0 0 0 0 0
#> median #DLT 2 0 0 0 0 0
#> min #DLT 1 0 0 0 0 0
#> max #DLT 3 0 0 0 0 0
#>
#> $`results mono1.a`
#> underdose target dose overdose max n reached before MTD all doses too toxic
#> Number of trials 0 1.00000 0 0 2.00000
#> Percentage 0 33.33333 0 0 66.66667
#> Percentage not all too toxic 0 100.00000 0 0 NA
#>
#> $`summary mono1.a`
#> Median Mean Min. Max. 2.5% 97.5%
#> #Pat underdose 0.0000000 0.0000000 0.00 0.0000000 0.0000000 0.0000000
#> #Pat target dose 9.0000000 8.0000000 3.00 12.0000000 3.3000000 11.8500000
#> #Pat overdose 0.0000000 0.0000000 0.00 0.0000000 0.0000000 0.0000000
#> #Pat (all) 9.0000000 8.0000000 3.00 12.0000000 3.3000000 11.8500000
#> #DLT underdose 0.0000000 0.0000000 0.00 0.0000000 0.0000000 0.0000000
#> #DLT target dose 3.0000000 2.3333333 1.00 3.0000000 1.1000000 3.0000000
#> #DLT overdose 0.0000000 0.0000000 0.00 0.0000000 0.0000000 0.0000000
#> #DLT (all) 3.0000000 2.3333333 1.00 3.0000000 1.1000000 3.0000000
#> % overdose 0.0000000 0.0000000 0.00 0.0000000 0.0000000 0.0000000
#> % DLT 0.3333333 0.3055556 0.25 0.3333333 0.2541667 0.3333333
#>
#> $`MTDs mono1.a`
#> 2 4 8
#> Dose 2 4 8
#> True P(DLT) 0.2 0.3 0.5
#> True category target dose target dose overdose
#> MTD declared (n) 1 0 0
#>
#> $`#pat. mono1.a`
#> 2 4 8
#> Dose 2 4 8
#> mean #pat 5 3 0
#> median #pat 6 3 0
#> min #pat 3 0 0
#> max #pat 6 6 0
#>
#> $`#DLT mono1.a`
#> 2 4 8
#> Dose 2 4 8
#> mean #DLT 0.666666666666667 1.66666666666667 0
#> median #DLT 1 2 0
#> min #DLT 0 0 0
#> max #DLT 1 3 0
#>
#> $prior
#> mean SD
#> mu_a1 -0.7081851 2.000000
#> mu_b1 0.0000000 1.000000
#> mu_a2 -0.7081851 2.000000
#> mu_b2 0.0000000 1.000000
#> mu_eta 0.0000000 1.121000
#> tau_a1 -1.3862944 0.707293
#> tau_b1 -2.0794415 0.707293
#> tau_a2 -1.3862944 0.707293
#> tau_b2 -2.0794415 0.707293
#> tau_eta -2.0794415 0.707293
#>
#> $specifications
#> Value -
#> seed 830451341 -
#> dosing.intervals 0.16 0.33
#> esc.rule ewoc -
#> esc.comp.max 1 -
#> dose.ref1 8 -
#> dose.ref2 2 -
#> saturating FALSE -
#> ewoc.threshold 0.25 -
#> start.dose.mono1.a 2 -
#> esc.step.mono1.a 2 -
#> esc.constrain.mono1.a FALSE -
#> max.n.mono1.a 12 -
#> cohort.size.mono1.a 3 -
#> cohort.prob.mono1.a 1 -
#> mtd.decision.mono1.a$target.prob 0.5 -
#> mtd.decision.mono1.a$pat.at.mtd 6 -
#> mtd.decision.mono1.a$min.pat 12 -
#> mtd.decision.mono1.a$min.dlt 1 -
#> mtd.decision.mono1.a$rule 2 -
#> mtd.enforce.mono1.a FALSE -
#> backfill.mono1.a FALSE -
#> backfill.size.mono1.a 3 -
#> backfill.prob.mono1.a 1 -
#> backfill.start.mono1.a 2 -
#> start.dose.combi.a1 2 -
#> start.dose.combi.a2 1 -
#> esc.step.combi.a1 2 -
#> esc.step.combi.a2 2 -
#> esc.constrain.combi.a1 FALSE -
#> esc.constrain.combi.a2 FALSE -
#> max.n.combi.a 24 -
#> cohort.size.combi.a 3 -
#> cohort.prob.combi.a 1 -
#> mtd.decision.combi.a$target.prob 0.5 -
#> mtd.decision.combi.a$pat.at.mtd 6 -
#> mtd.decision.combi.a$min.pat 12 -
#> mtd.decision.combi.a$min.dlt 1 -
#> mtd.decision.combi.a$rule 2 -
#> mtd.enforce.combi.a FALSE -
#> backfill.combi.a FALSE -
#> backfill.size.combi.a 3 -
#> backfill.prob.combi.a 1 -
#> backfill.start.combi.a1 2 -
#> backfill.start.combi.a2 1 -
#>
#> $`Stan options`
#> Value
#> chains 4.0
#> iter 13500.0
#> warmup 1000.0
#> adapt_delta 0.8
#> max_treedepth 15.0
Here, only 3 studies are simulated for illustration purposes. Please
note, for simulations, one can also initiate storing and re-loading of
MCMC results to speed up simulations further when parallelizing on a
moderate number of cores (e.g., when one is not using a cluster). For
details, refer to the argument working.path
of the function
sim_jointBLRM()
.
Parallelization on a cluster
All of the functions of the decider
package that allow
for parallelization feature a nested foreach
loop with two
stages. With this, is is possible to leverage the capabilities of
multiple cluster nodes that each have multiple cores. Usually,
simulations of joint BLRM trials with multiple arms will take a
relatively long run time (up to the order of hours) when executed on a
regular computer with a moderate amount of cores, so running simulations
on a cluster may be desirable to achieve manageable runtimes. Please
note that function sim_jointBLRM
parallelizes across
trials, so, when simulating e.g. 1000 trials, multiple hundreds cores
can be used to substantially improve the run time.
In the following, it is illustrated how an exemplary parallel backend
on a common SLURM cluster could be registered. We will use the
future.batchtools
package for this:
Let us assume we have a cluster available where we want to request 10 nodes with 32 cores per node (320 cores in total). We specify this and further ressources like the job time and memory requested in the following arguments:
#number of processor nodes
n_nodes <- 10
#number of cores per node
n_cpus <- 32
#job time in hours
walltime_h <- 2
#memory requested in GB
memory_gb <- 4
With this, we can allocate the parallel backend with the given specifications:
slurm <- tweak(batchtools_slurm,
template = system.file('templates/slurm-simple.tmpl', package = 'batchtools'),
workers = n_nodes,
resources = list(
walltime = 60 * 60 * walltime_h,
ncpus = n_cpus,
memory = 1000 * memory_gb))
#register paralel backend on cluster
registerDoFuture()
plan(list(slurm, multisession))
With this specification, when e.g. sim_jointBLRM()
is
called, the trials that need to be simulated will first be divided
evenly across the available nodes, and the trials to be simulated by
each node will be distributed across the CPUs the node has. In the above
example, when we simulate e.g. 6400 trials using 20 nodes with 32 cores
each, each node will receive 320 trials, which will be split across 32
cores, so that each core will need to simulate only 10 of the
trials.
After this setup, the sim_jointBLRM()
function can be
called in the same way as illustrated previously.
Please note that there are some tradeoffs involed depending on the system, e.g. requesting a very large number of cores may take more time.
At last, please note that the function sim_jointBLRM()
also supports the use of a working.path
argument which
allows to enable saving and reloading of MCMC results. If this is
executed on a parallel backend, please note that the path must be
somewhere in the shared memory, where all CPUs have access.
Synchronization across CPUs is carried out using file locks, which may
introduce some overhead. It was observed that this feature can
potentially still provide significant benefits to the runtime (both on a
cluster and on a standard computer), but please be aware that this is
highly experimental.