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 PTXsm_35; the driver has just JIT-compiled it for thesm_86device above. The first run on a new node spends a few seconds in this compilation step; subsequent calls reuse the cache under$CUDA_CACHE_PATHand start almost instantly.kernel complete: 93 msis the pure GPU time;saving data complete: 144 msincludes the post-processing (normalization, zlib compression, file I/O).MCX simulation speed: 31250 photon/msis the headline performance metric. RTX 3090 gives roughly 30k photon/ms oncube60; 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.
#!/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.
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:
#!/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 ismcxlabechoing how it translated the MATLABcfgstruct 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=10reflects the 10 time bins implied bytstep=5e-10over[0, 5e-9]. The cube60 benchmark in the previous section ran withmaxgate=1because that bench uses a single CW bin; hereflux.datawill be a60×60×60×10array.MCX simulation speed: 20833 photon/msis 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 incfg.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:
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:
#!/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%andmaxgate=10match themcxlabrun on the same slab; same engine, same configuration, same result.MCX simulation speed: 19231 photon/msis slightly below themcxlabnumber (~21k) on the same RTX 3090. Thepmcxwheel ships its own MCX C build (notecode name: [Fermi MCX]andcompiled 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
pmcxconfig echo at the top of stdout (nphoton: 1e+06,tstart: 0, ...) ispmcxreporting how it parsed thecfgdict, useful for debugging silent default substitutions; same idea as themcx.xxx=...block in themcxlablog.
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¶
-
skinvessel walkthrough
Full worked example using the canonical MMC benchmark: copy, run in batch, analyze in MATLAB.
-
Software catalog
Versions installed across the three clusters.
-
VNC remote desktop
Recommended way to run
mcxstudio,mcxshow, andmcxviewer. -
GPU accelerators
GPU inventory,
--constraintvalues, and SLURM examples. -
MCX wiki
Upstream documentation, input-file reference, and tutorials.