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).