knitr::opts_chunk$set( collapse = TRUE, warning = FALSE, message = FALSE, comment = "#>" ) library(logitr)
Mixed logit (MXL) models are estimated with maximum simulated likelihood, which repeatedly evaluates the log-likelihood over a set of random draws. For models with many random parameters or many draws, this can be slow.
By default, mixed logit models are estimated with a fast compiled, multi- threaded backend, so you do not need to do anything to get good performance. This vignette explains what that backend is and the arguments that control it:
backend: the compiled C++ backend ("cpp", the default for MXL) versus the
native R implementation ("cpu").numThreads: how many CPU cores to use for the parallel evaluation.numDrawsBatch: streaming the draws in batches to bound memory for very large
draw counts.These change only how the log-likelihood is computed, not what is computed: all options give the same estimates to floating-point precision.
backend)For mixed logit models, logitr() uses backend = "cpp" by default: a compiled
C++ implementation of the log-likelihood and its analytic gradient. You do not
need to do anything to get it — the model below already uses it:
model <- logitr( data = yogurt, outcome = "choice", obsID = "obsID", panelID = "id", pars = c("price", "feat", "brand"), randPars = c(feat = "n") )
The "cpp" backend supports all mixed logit model types: preference and
willingness-to-pay (WTP) space, uncorrelated and correlated heterogeneity, and
normal ("n"), log-normal ("ln"), and censored-normal ("cn") parameter
distributions. It is typically about 4 times faster than the native R
implementation.
To use the native R implementation instead, set backend = "cpu". It returns
the same coefficients, standard errors, and log-likelihood as "cpp" to
floating-point precision. The main reason to choose it is exact
bit-reproducibility (see the section on numThreads below). Multinomial logit
(MNL) models always use the R implementation, since they are already fast.
Because it includes compiled code, installing the development version of {logitr} from source requires a C++ compiler (see the installation instructions). Installing the released version from CRAN does not, since CRAN provides pre-built binaries.
numThreads)The "cpp" backend processes the random draws in parallel across CPU cores.
Since the draws are independent, this is an exact parallelization that scales
well with the number of cores. You can control the number of threads directly:
model <- logitr( data = yogurt, outcome = "choice", obsID = "obsID", panelID = "id", pars = c("price", "feat", "brand"), randPars = c(feat = "n"), numDraws = 500, numThreads = 4 )
By default (numThreads = NULL), all but one of the available cores are used,
except when running a parallel multistart (numMultiStarts > 1): in that case a single
thread is used per model so that the cores go to the multistart instead of
being oversubscribed by nested parallelism. Set numThreads = 1 to disable
threading entirely.
Because threading uses a parallel reduction, the summation happens in a
non-deterministic order, so results are not bit-identical across runs —
they differ only at the level of floating-point rounding (around 1e-12), far
below the optimization tolerance. If you need exactly reproducible results,
set numThreads = 1 (or backend = "cpu").
The table below shows the speedup for evaluating the mixed logit log-likelihood and gradient once, for a panel model with random parameters, as the number of draws grows. Speedups are relative to the native R backend, measured on a 10-core machine (using 9 threads for the multithreaded column). Notice that the multithreaded speedup grows with the number of draws, because larger draw counts give the threads more work to distribute.
| Draws | cpu | cpp (1 thread) | cpp (multithreaded) |
|-------:|:-----:|:----------------:|:---------------------:|
| 100 | 1x | ~4x | ~21x |
| 500 | 1x | ~4x | ~27x |
| 2,000 | 1x | ~4x | ~27x |
| 10,000 | 1x | ~4x | ~33x |
You can reproduce a version of this comparison on your own machine with the
bench/perf_compare.R script in the package's GitHub repository.
numDrawsBatch)The compiled "cpp" backend (the default) is memory-flat in the number of
draws, so it handles very large draw counts without any special settings. The
numDrawsBatch argument is only relevant when you use the native R backend
(backend = "cpu"), which by default stores intermediate quantities for every
draw at once, so its memory grows with numDraws. For very large draw counts
that can exhaust memory. Setting numDrawsBatch streams the draws in batches,
keeping peak memory bounded by the batch size rather than the total number of
draws:
model <- logitr( data = yogurt, outcome = "choice", obsID = "obsID", panelID = "id", pars = c("price", "feat", "brand"), randPars = c(feat = "n"), numDraws = 10000, numDrawsBatch = 500, backend = "cpu" )
By default (numDrawsBatch = NULL), the "cpu" backend streams automatically
only when the draws would otherwise exceed an internal memory budget, so typical
models are unaffected and use the faster non-streaming path.
logitr does not provide a GPU backend. For the models and dataset sizes typical of choice modeling, the compiled, multithreaded CPU backend is already very fast, and benchmarking shows that a GPU offers little benefit for this kind of computation (the log-likelihood is dominated by a "skinny" matrix multiplication and irregular segment sums, which are memory-bound rather than compute-bound) — and on integrated GPUs it can even be slower than the CPU. A GPU only tends to help on a dedicated NVIDIA GPU with a very large dataset.
If you specifically need GPU-accelerated mixed logit estimation for very large problems, the Python package xlogit is purpose-built for it and supports CUDA GPUs directly.
"cpp" backend across all but one of the available cores by default, and it
handles large draw counts without running out of memory."cpp" backend is
memory-flat in the number of draws, and the multithreaded speedup is largest
in exactly this regime.numThreads = 1 (or backend = "cpu") if you need exactly reproducible,
bit-identical results, at the cost of some speed.backend = "cpu" if you want the native R implementation for any reason
(for example, to compare against the compiled backend).Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.