Skip to content

MCX

Monte Carlo eXtreme (MCX) is a GPU-accelerated photon transport simulator for 3D heterogeneous turbid media. It is the standard tool in biomedical optics for forward modelling in diffuse optical tomography (DOT), functional near-infrared spectroscopy (fNIRS), and tissue optics in general.

The module ships the complete MCXStudio distribution: the CUDA voxel engine, an OpenCL fallback, a mesh-based engine, MATLAB/Octave bindings, a meshing toolbox, an inverse-problem solver, and the Lazarus/GTK2 GUI. Loading a single module makes all of them available on PATH and MATLABPATH.

GPU support

MCX uses the system NVIDIA driver directly; there is no module load CUDA step. The binary embeds PTX sm_35 and the driver JIT-compiles it on first launch for the actual GPU (Pascal through Blackwell). All Hyperion GPUs are supported: A100 SXM4/PCIe (sm_80), RTX 3090 / RTX A6000 (sm_86), A30 (sm_80), and RTX PRO 6000 Blackwell (sm_120). See GPU accelerators for --constraint values.

For a complete worked example using the canonical skinvessel benchmark, including the MCXStudio GUI and MATLAB analysis, jump to the skinvessel walkthrough.

The MCX suite

Tool What it does When to use it
mcx Voxel-based Monte Carlo on CUDA. Primary engine. Default choice for any voxelized geometry on NVIDIA GPUs.
mcxcl Voxel-based Monte Carlo on OpenCL. Cross-vendor fallback; slower than mcx on NVIDIA hardware.
mmc Mesh-based Monte Carlo on tetrahedral domains. Curved boundaries or thin layers where voxels are too coarse.
mcxstudio Lazarus/GTK2 GUI front-end to design, run, and visualize simulations. Interactive exploration; pairs with VNC.
mcxshow OpenGL volumetric viewer. Inspect input volumes.
mcxviewer OpenGL fluence viewer. Inspect output fluence maps.
mcxlab MATLAB/Octave bindings for mcx. Script CUDA voxel simulations from MATLAB.
mcxlabcl MATLAB/Octave bindings for mcxcl. Same, OpenCL backend.
mmclab MATLAB/Octave bindings for mmc. Mesh-based MC from MATLAB.
iso2mesh Tetrahedral mesh generation toolbox (required by mmc). Build meshes from segmented volumes or analytic geometries.
redbird FEM diffusion forward/inverse solver. DOT image reconstruction.

Loading the module

module load MCX/2025.10

To see all installed versions across clusters, check the software catalog or run module spider MCX.

Expand to see all available modules on each cluster
MCX/2025.10

Not available on this cluster.

Not available on this cluster.

The module sets these convenience variables:

Variable Points to
$MCX_ROOT Install root (also exposed as $EBROOTMCX).
$MCX_EXAMPLES Voxel MCX input examples and benchmarks.
$MMC_EXAMPLES Parent of examples/ for mesh-based MMC.
$MCXCL_EXAMPLES OpenCL examples.
$MCXSTUDIO_DEMO Demo .mcxp projects to open from the GUI.
$ISO2MESH_BIN Compiled iso2mesh helpers (cgalmesh, tetgen, jmeshlib, ...).
$MATLABPATH Pre-populated with mcxlab, mmclab, mcxlabcl, iso2mesh, redbird.
$CUDA_CACHE_PATH PTX JIT cache, redirected to $TMPDIR or /tmp to avoid NFS overhead.

Quick check on a GPU node

Before writing a full batch script, request a short interactive GPU session and check that MCX sees the device. This is also the easiest way to confirm that PTX JIT compilation worked on this particular node.

srun --qos=regular --partition=general --gres=gpu:1 --cpus-per-task=4 --mem=16G --time=00:30:00 --pty bash

module load MCX/2025.10
mcx -L

On an RTX 3090 node the output looks like:

=============================   GPU Information  ================================
Device 1 of 1:      NVIDIA GeForce RTX 3090
Compute Capability: 8.6
Global Memory:      25296044032 B
Constant Memory:    65536 B
Shared Memory:      49152 B
Registers:      65536
Clock Speed:        1.70 GHz
Number of SMs:      82
Number of Cores:    5248
Auto-thread:        335872
Auto-block:     64

Two things to verify: the device line matches the GPU you requested, and Compute Capability is what you expect (8.0 for A100, 8.6 for RTX 3090 / A6000, 12.0 for the Blackwell RTX PRO 6000). Auto-thread and Auto-block are the values that mcx -A (autopilot) will plug in later.

Then run the canonical benchmark:

mcx --bench cube60

cube60 is a homogeneous 60×60×60 voxel cube with a pencil beam at the center of the top face. MCX prints a full run summary:

###############################################################################
#                      Monte Carlo eXtreme (MCX) -- CUDA                      #
###############################################################################
[copyright, funding and NeuroJSON banner omitted]
$Rev::4d6ca7$v2025.10$Date::2025-10-12 12:45:50 -04$ by $Author::Qianqian Fang$
- code name: [Kilo-Kelvin] compiled by nvcc [7.5] for CUDA-arch [350] on [Oct 12 2025]
- compiled with: RNG [xorshift128+] seed length [4]

GPU=1 (NVIDIA GeForce RTX 3090) threadph=2 extra=328256 np=1000000 nthread=335872 maxgate=1 repetition=1
initializing streams ...    init complete : 1 ms
launching MCX simulation for time window [0.00e+00ns 5.00e+00ns] ...
simulation run# 1 ...
kernel complete:      93 ms
retrieving fields ...     detected 3006 photons, total: 3006    transfer complete:    103 ms
normalizing raw data ...    source 1, normalization factor alpha=200.000000
data normalization complete : 109 ms
saving data to file ...
saving data complete : 144 ms

simulated 1000000 photons (1000000) with 335872 threads (repeat x1)
MCX simulation speed: 31250.00 photon/ms
total simulated energy: 1000000.00    absorbed: 17.84726%

A few lines worth knowing how to read:

  • CUDA-arch [350] confirms that the binary embeds PTX sm_35; the driver has just JIT-compiled it for the sm_86 device above. The first run on a new node spends a few seconds in this compilation step; subsequent calls reuse the cache under $CUDA_CACHE_PATH and start almost instantly.
  • kernel complete: 93 ms is the pure GPU time; saving data complete: 144 ms includes the post-processing (normalization, zlib compression, file I/O).
  • MCX simulation speed: 31250 photon/ms is the headline performance metric. RTX 3090 gives roughly 30k photon/ms on cube60; A100 PCIe and SXM4 are higher. Use this as a quick baseline when picking a GPU type for production runs.
  • absorbed: 17.85% is the expected fraction for the cube60 medium properties; if you see something very different it usually means the input was loaded incorrectly.

Batch job example (CLI)

The simplest "real" run is the bundled cube60 benchmark launched as a batch job. The example below uses a single A100 PCIe GPU, but any Hyperion GPU works.

mcx.slurm
#!/bin/bash
#SBATCH --qos=regular
#SBATCH --partition=general
#SBATCH --job-name=mcx
#SBATCH --nodes=1
#SBATCH --ntasks=1
#SBATCH --cpus-per-task=4
#SBATCH --gres=gpu:1
#SBATCH --constraint=a100-pcie
#SBATCH --mem=16G
#SBATCH --time=00:30:00
#SBATCH --output=%x.%j.out
#SBATCH --error=%x.%j.err

module load MCX/2025.10

# -G : GPU index (1-based, first visible GPU)
# -A : autopilot (let mcx pick thread / block size from the GPU model)
mcx --bench cube60 -G 1 -A

To run your own JSON input, replace the last line with mcx -f input.json -G 1 -A -s output_id. The structure of an MCX JSON input and a fully annotated example live in the skinvessel walkthrough.

Pick the GPU that fits your photon budget

Voxel MCX needs very little device memory by itself; the constraint is usually photon throughput. For large volumes or many detectors, prefer A100 (80 GB) or RTX PRO 6000 (96 GB). For many small parameter sweeps, RTX 3090 nodes give you eight independent GPUs across hyperion-030/052/074/.... See the GPU page for --constraint values.

Batch job example (mcxlab from MATLAB)

The module pre-populates MATLABPATH, so as soon as you start MATLAB you can call mcxlab(cfg) without a single addpath. The example below is the homogeneous slab used to validate the engine: a 60x60x60 voxel cube with a pencil source at one face.

test.m
cfg.nphoton   = 1e6;
cfg.vol       = uint8(ones(60, 60, 60));
cfg.srcpos    = [30 30 1];
cfg.srcdir    = [0 0 1];
cfg.gpuid     = 1;
cfg.autopilot = 1;
cfg.prop      = [0 0 1 1; 0.005 1 0.01 1.37];
cfg.tstart    = 0;
cfg.tend      = 5e-9;
cfg.tstep     = 5e-10;

flux = mcxlab(cfg);

cfg.vol is the segmentation volume (all voxels tagged 1 here), cfg.prop lists [mua, mus, g, n] per tag (row 1 is always reserved for tag 0, the ambient medium), and cfg.gpuid is 1-indexed, matching the device numbering you see from mcx -L. The output flux.data is a 4D array [nx, ny, nz, nt] with 10 time frames in this configuration.

Run it from a SLURM job:

mcxlab.slurm
#!/bin/bash
#SBATCH --qos=regular
#SBATCH --partition=general
#SBATCH --job-name=mcxlab
#SBATCH --nodes=1
#SBATCH --ntasks=1
#SBATCH --cpus-per-task=4
#SBATCH --gres=gpu:1
#SBATCH --mem=16G
#SBATCH --time=00:30:00
#SBATCH --output=%x.%j.out
#SBATCH --error=%x.%j.err

module load MATLAB/2024b-IKUR
module load MCX/2025.10

matlab -nodisplay -nosplash -batch "run('test.m')"

The job log (mcxlab.<JOBID>.out) looks like this:

Launching MCXLAB - Monte Carlo eXtreme for MATLAB & GNU Octave ...
Running simulations for configuration #1 ...
mcx.nphoton=1e+06;
mcx.dim=[60 60 60];
mcx.mediabyte=1;
mcx.srcpos=[30 30 1 1];
mcx.srcdir=[0 0 1 0];
mcx.gpuid=1;
mcx.autopilot=1;
mcx.medianum=2;
mcx.tstart=0;
mcx.tend=5e-09;
mcx.tstep=5e-10;
###############################################################################
#                      Monte Carlo eXtreme (MCX) -- CUDA                      #
###############################################################################
[banner omitted]
- code name: [Kilo-Kelvin] compiled by nvcc [7.5] for CUDA-arch [350] on [Oct 12 2025]
- compiled with: RNG [xorshift128+] seed length [4]

GPU=1 (NVIDIA GeForce RTX 3090) threadph=2 extra=328256 np=1000000 nthread=335872 maxgate=10 repetition=1
initializing streams ...    init complete : 6 ms
launching MCX simulation for time window [0.00e+00ns 5.00e+00ns] ...
simulation run# 1 ...
kernel complete:      114 ms
retrieving fields ...     transfer complete:    131 ms
normalizing raw data ...    source 1, normalization factor alpha=2000.000000
data normalization complete : 165 ms
simulated 1000000 photons (1000000) with 335872 threads (repeat x1)
MCX simulation speed: 20833.33 photon/ms
total simulated energy: 1000000.00    absorbed: 27.35144%

A few things to read out of this log:

  • The opening mcx.nphoton=...; mcx.dim=...; ... block is mcxlab echoing how it translated the MATLAB cfg struct into MCX's internal config. It is the first place to look when the result is not what you expected: any silent default substitution shows up here.
  • maxgate=10 reflects the 10 time bins implied by tstep=5e-10 over [0, 5e-9]. The cube60 benchmark in the previous section ran with maxgate=1 because that bench uses a single CW bin; here flux.data will be a 60×60×60×10 array.
  • MCX simulation speed: 20833 photon/ms is lower than the cube60 number above on the same GPU because each photon is now binned across ten time gates instead of accumulating into a single frame; the kernel does strictly more work per photon.
  • absorbed: 27.35% is specific to the optical properties in cfg.prop (mua=0.005, mus=1) over the 60×60×60 slab.

Batch job example (pmcx from Python)

pmcx provides native Python bindings for MCX. The wheel published on PyPI does not pull numpy automatically, so install both together. Into your user site-packages:

module load MCX/2025.10 Miniforge3
conda deactivate
pip install --user pmcx numpy

Or into an isolated virtualenv:

module load MCX/2025.10 Miniforge3
conda deactivate
python -m venv ~/venvs/mcx
source ~/venvs/mcx/bin/activate
pip install pmcx numpy

The conda deactivate step leaves the miniforge3-base environment so the new install is clean and does not inherit packages from it; see Python on the clusters for the full Miniforge3 workflow.

The Python API mirrors mcxlab closely. The same homogeneous slab from the MATLAB example above looks like this in Python:

test.py
import pmcx
import numpy as np

cfg = {
    'nphoton':   1000000,
    'vol':       np.ones((60, 60, 60), dtype=np.uint8),
    'srcpos':    [30, 30, 1],
    'srcdir':    [0, 0, 1],
    'gpuid':     1,
    'autopilot': 1,
    'prop':      [[0, 0, 1, 1], [0.005, 1, 0.01, 1.37]],
    'tstart':    0,
    'tend':      5e-9,
    'tstep':     5e-10,
}

result = pmcx.run(cfg)
flux   = result['flux']
print(f'flux shape   : {flux.shape}')
print(f'total fluence: {flux.sum():.4e}')

The cfg dictionary uses the same field names as the MATLAB cfg struct: anything documented for mcxlab works in pmcx with cfg.field rewritten as cfg['field']. pmcx.run(cfg) returns a dictionary; the volumetric fluence lives in result['flux'] as a numpy array of shape [nx, ny, nz, nt], the same 60×60×60×10 array mcxlab returns in flux.data.

Submit it from a SLURM job:

pmcx.slurm
#!/bin/bash
#SBATCH --qos=regular
#SBATCH --partition=general
#SBATCH --job-name=pmcx
#SBATCH --nodes=1
#SBATCH --ntasks=1
#SBATCH --cpus-per-task=4
#SBATCH --gres=gpu:1
#SBATCH --mem=16G
#SBATCH --time=00:30:00
#SBATCH --output=%x.%j.out
#SBATCH --error=%x.%j.err

module load MCX/2025.10 Miniforge3
conda deactivate
source ~/venvs/mcx/bin/activate

python test.py

The job output is split across the two SLURM files in a way that surprises people the first time. The print calls in test.py and the pmcx config echo land in stdout (pmcx.<JOBID>.out); the MCX C engine banner and run summary go to stderr (pmcx.<JOBID>.err). This is how native Python extensions wrapping a C library normally behave: the C runtime writes to its own stderr, untouched by Python's sys.stdout. Nothing is failing; the simulation log is in the .err file.

$ cat pmcx.<JOBID>.out
nphoton: 1e+06
tstart: 0
tstep: 5e-10
tend: 5e-09
autopilot: 1
flux shape   : (60, 60, 60, 10)
total fluence: 1.0941e+11
$ cat pmcx.<JOBID>.err
[Miniforge3 banner omitted]
###############################################################################
#                      Monte Carlo eXtreme (MCX) -- CUDA                      #
###############################################################################
[banner omitted]
$Rev::f4836d$v2025.10$Date::2026-04-06 17:43:01 -04$ by $Author::Qianqian Fang$
- code name: [Fermi MCX] compiled by nvcc [9.2] for CUDA-arch [350] on [Apr  6 2026]
- compiled with: RNG [xorshift128+] seed length [4]

GPU=1 (NVIDIA GeForce RTX 3090) threadph=2 extra=328256 np=1000000 nthread=335872 maxgate=10 repetition=1
initializing streams ...    init complete : 2 ms
launching MCX simulation for time window [0.00e+00ns 5.00e+00ns] ...
simulation run# 1 ...
kernel complete:      113 ms
retrieving fields ...     transfer complete:    121 ms
normalizing raw data ...    source 1, normalization factor alpha=2000.000000
data normalization complete : 145 ms
simulated 1000000 photons (1000000) with 335872 threads (repeat x1)
MCX simulation speed: 19230.77 photon/ms
total simulated energy: 1000000.00    absorbed: 27.35144%

A few things to read out of these:

  • absorbed: 27.35% and maxgate=10 match the mcxlab run on the same slab; same engine, same configuration, same result.
  • MCX simulation speed: 19231 photon/ms is slightly below the mcxlab number (~21k) on the same RTX 3090. The pmcx wheel ships its own MCX C build (note code name: [Fermi MCX] and compiled by nvcc [9.2] vs the module's [Kilo-Kelvin] / nvcc [7.5]), so they are not literally the same binary; small performance differences across upstream snapshots are normal.
  • The pmcx config echo at the top of stdout (nphoton: 1e+06, tstart: 0, ...) is pmcx reporting how it parsed the cfg dict, useful for debugging silent default substitutions; same idea as the mcx.xxx=... block in the mcxlab log.

Refer to upstream documentation in $EBROOTMCX/share/doc/mcx/ for the full pmcx API reference.

Mesh-based Monte Carlo (mmc)

For geometries with curved interfaces or thin layers that voxels cannot resolve well (vessels, sulci, microfluidic channels) use mmc on a tetrahedral mesh. The workflow is to build a mesh, typically with iso2mesh from MATLAB (v2m, s2m, ...), save it in MMC's node/element format, and launch mmc -f mesh_input.json. Ready-made examples live under $MMC_EXAMPLES/examples/:

$ ls $MMC_EXAMPLES/examples
colin27  mcxsph    misctest   onecube  README.txt  regression  replaywide  sfdi2layer  skinvessel  sphshells  statnoise   widedet
dcs      meshtest  mmclbench  planar   reftest     replay      rngtest     sharing     sphere      ssehmin    validation

A few good starting points: planar (slab geometry, simplest sanity test), sphshells (multi-layer spherical phantom), skinvessel (NIRS-style geometry with a blood vessel), and validation (analytic reference cases). Each subfolder contains an input JSON, a mesh, and a run_test.sh driver:

cp -r $MMC_EXAMPLES/examples/sphshells ~/mmc_sphshells
cd ~/mmc_sphshells
bash run_test.sh

See the official MMC wiki for input-file syntax.

MCXStudio (GUI)

mcxstudio is a Lazarus/GTK2 application; the 3D viewers (mcxshow, mcxviewer) on top of it use OpenGL. None of this works through plain ssh -X with any reasonable responsiveness. The supported path on our clusters is to run it inside a VNC remote desktop session, where the GTK toolkit, OpenGL, and the GPU all live on the same node as the user interface.

The skinvessel walkthrough covers the full GUI workflow step by step, with a recorded screencast.

Tips and tuning

Multi-GPU runs

-G selects which GPUs mcx uses. A bitmask of 0/1 digits, read left to right starting at GPU 1, marks which devices are active (device numbering matches mcx -L):

mcx -f input.json -G 11 -A        # GPUs 1 and 2
mcx -f input.json -G 1111 -A      # first 4 GPUs
mcx -f input.json -G 0011 -A      # GPUs 3 and 4 (skip the first two)

Add --gres=gpu:N (and the matching --constraint) to the SLURM script. Multi-GPU pays off when single-GPU time dominates JIT, copy, and launch overhead; in practice, for runs of 1e9 photons or more.

CUDA JIT cache

The module already redirects CUDA_CACHE_PATH to $TMPDIR/cuda_cache (or /tmp/$USER/cuda_cache if no $TMPDIR is set), so the JIT cache stays off NFS and survives between job steps on the same node. For long-running campaigns of many small MCX jobs on the same node, consider using /lscratch so the cache is on local disk and is not flushed between jobs.

Troubleshooting

mcx -L reports no GPUs

You are most likely on a login node or on a CPU-only compute node. MCX needs a real GPU and only nvidia-smi-visible devices count. Confirm with nvidia-smi first, and make sure your job requests --gres=gpu:N. If the device exists but is reported as compute-prohibited, another job has it in exclusive mode; submit elsewhere or wait.

First launch takes 30+ seconds

Expected. The driver is JIT-compiling the embedded PTX sm_35 code for the actual compute capability of the GPU (sm_80, sm_86, sm_120, ...). The result is cached under $CUDA_CACHE_PATH, which the module redirects to $TMPDIR/cuda_cache to keep it off NFS. Subsequent runs on the same node are sub-second. If you observe this on every run, check that $TMPDIR is writable and not full.

mcxstudio opens but the 3D viewer is blank or extremely slow

OpenGL applications need a hardware GL context to be responsive. SSH X11 forwarding falls back to software GLX, which is unusable for the volumetric viewers. Run MCXStudio inside a VNC desktop session on a GPU node. The Lazarus/GTK2 GUI itself (menus, project editor) works fine launched directly with mcxstudio &; the 3D preview windows and the standalone mcxshow / mcxviewer need hardware OpenGL, and you get that by prefixing the launch with vglrun -d egl:

vglrun -d egl mcxstudio &     # hardware-accelerated EGL backend
vglrun -d egl mcxviewer file.jnii

MATLAB error: mcx.mexa64 could not be loaded

Almost always one of two things: (i) you launched MATLAB before loading the module, so MATLABPATH was not set; quit MATLAB, module load MCX/2025.10, then restart MATLAB; (ii) MATLAB and mcx.mexa64 disagree on the glibc / libstdc++ versions. The MEX files in this module are built against the system libraries on Hyperion (Rocky Linux 8); on Atlas, the module may need rebuilding. Open a software request if mcxlab is unavailable on the cluster you need.

Results differ between two runs of the same input

By design. Monte Carlo simulations use random number streams; the default seed depends on the wall clock. Set cfg.seed = <integer> in MATLAB or "RNGSeed": <integer> in the JSON Session block for bit-exact reproducibility. Note that GPU non-determinism in atomic accumulators can still produce sub-floating-point-precision differences across runs even with a fixed seed.

See also