Multi-Threading API Reference

Reference for Mera's threading controls. The guide: what is parallel, what to expect, how to choose a thread count, is Multi-Threading.
Threads are set when Julia starts, not from inside a session:
julia -t 8 # or: export JULIA_NUM_THREADS=8Which functions are threaded, and over what
The functions below accept max_threads::Int to cap what they use, defaulting to Threads.nthreads(). The dimension each one parallelises over decides whether more threads help, a single-variable projection stays flat no matter how many you give it.
| Function | Parallel over |
|---|---|
gethydro, getparticles, getgravity, getrt | RAMSES CPU-file chunks |
projection on cells (hydro/gravity) | the variables requested in one call |
projection on particles | pixels (:voronoi) or particle chunks (:mass/:volume/:sph) |
clumpfind | candidate chunks |
export_vtk | particles / cells |
convertdata, batch_convert_mera | components being converted |
gas = gethydro(info, max_threads=4) # cap the read
projection(gas, [:sd, :T, :vx], :km_s, max_threads=3) # one task per variableUnlike the cell backend, particle projection does not parallelise over variables, it splits the work inside a single map, so one variable already benefits:
weighting=:voronoipartitions the pixels. Each ray is independent and each thread owns disjoint output pixels, so the result is bitwise identical to the serial one at any thread count.:mass,:volumeand:sphpartition the particles into chunks with per-thread accumulators, reduced in a fixed order. Reproducible for a given thread count; it differs from the serial sum only by floating-point association (~1e-15 relative).
:voronoi scales best because it is compute-bound; the deposition schemes are limited by memory bandwidth and gain roughly 2 to 4×.
Diagnostics
Mera.show_threading_info — Function
show_threading_info()Display information about Julia threading configuration and recommendations.
Benchmarking
Measure on your own data and storage rather than assuming, reading is usually I/O bound and saturates when the storage does.
Mera.benchmark_projection_hydro — Function
benchmark_projection_hydro(gas_data, thread_counts::Vector{Int}, n_runs::Int=10, output_file::String="") → DictExecute comprehensive AMR hydro projection benchmark with robust statistical analysis.
This function serves as the main coordinator for hydro projection performance testing. It performs AMR structure analysis, data quality validation, executes both single-variable and multi-variable projection benchmarks across specified thread counts, and exports results in multiple formats with comprehensive statistical analysis.
Benchmark Methodology
- Single-Variable Test: Surface density projection (:sd → Msun/pc²)
- Multi-Variable Test: 10 simultaneous variable projections: vars = [:v, :σ, :σx, :σy, :σz, :vrcylinder, :vϕcylinder, :σrcylinder, :σϕcylinder, :cs]
- Statistical Robustness: several repetitions per configuration with coefficient of variation
- Quality Control: Success rate monitoring (>80% threshold for reliable data)
- Memory Profiling: Peak memory usage and garbage collection analysis
Threading Analysis
Evaluates performance across thread counts with derived metrics:
- Speedup: Performance improvement vs single-threaded execution
- Efficiency: Speedup per thread (percentage of ideal scaling)
- Memory Scaling: Memory usage patterns across thread configurations
Output Files Generated
{output_file}.csv: Structured data for spreadsheet analysis and plotting{output_file}.json: Machine-readable structured data for programmatic access{output_file}_summary.txt: Human-readable performance report with insights
Arguments
gas_data: HydroDataType object from loaddata() or gethydrodata()thread_counts::Vector{Int}: Thread counts to benchmark [1, 2, 4, 8, 16, ...]n_runs::Int=10: Statistical repetitions per configuration (10 for robust analysis)output_file::String="": Output filename base (auto-generated timestamp if empty)
Returns
Dictionary containing complete benchmark results with keys:
n_threads,test_type,mean_time,std_time,speedup,efficiencymean_memory,success_rate,min_time,max_time,n_runs
Example Usage
# Load RAMSES hydro data
gas_data = loaddata(300, "/path/to/ramses/output/", :hydro)
# Run comprehensive benchmark (single + multi-variable)
results = benchmark_projection_hydro(gas_data, [1, 2, 4, 8, 16], 10, "performance_test")
# Results saved as:
# - performance_test.csv (for plotting with plot_results.jl)
# - performance_test.json (for programmatic analysis)
# - performance_test_summary.txt (human-readable report)Performance Insights
The benchmark automatically analyzes threading efficiency and provides guidance:
- Identifies optimal thread counts for your system and data size
- Detects threading bottlenecks and memory constraints
- Quantifies single vs multi-variable projection performance differences
- Provides statistical confidence intervals for all measurements
Integration Workflow
- Data Loading: Use Mera's loaddata() for your RAMSES simulation
- Benchmarking: Execute this function with desired thread counts
- Visualization: Use plot_results.jl to create performance dashboards
- Analysis: Review summary.txt for optimization recommendations
Mera.run_reading_benchmark — Function
run_reading_benchmark(output_number, path)Time reading one RAMSES output under the current thread configuration and save the result.
Used to produce the parallel RAMSES-reading benchmark in the documentation; run it once per thread setting to build the scaling curve.
Mera.reading_sweep — Function
reading_sweep(output, path; threads, component=:hydro, runs=2, lmax=missing, kwargs...)Find the thread count that reads your data fastest, on your storage.
Reads one component repeatedly at each thread count in threads and reports a table of times, speedups and efficiency, then names the point past which more threads stop helping. That point is the number to use for the rest of your analysis.
Defaults to :hydro because it dominates the total: on the reference snapshot it is 43.3 s of a 49.2 s read, so sweeping all three components costs four times as much for a slightly better estimate of the same answer.
Cost
This is the expensive benchmark. It performs length(threads) * runs full reads of the component, plus one untimed warm-up. It prints that count and asks nothing, so bound it yourself on a large snapshot: pass lmax, a subregion through kwargs, or fewer threads values.
Keywords
threads: thread counts to test. Defaults to a1, 2, 4, 8, ...ladder capped atmax_threads, or atmin(Threads.nthreads(), allocated_cpus())when that is unset.max_threads: ceiling for the default ladder.component::hydro,:gravityor:particles.runs: repetitions per thread count. The minimum is the reported estimate, since the fastest run is the one least disturbed by other load on the node, but every run is kept and the spread between fastest and slowest is reported alongside it. A spread of a few percent means the node was quiet; a large one means it was not, and the numbers should be treated accordingly.lmax, and any selection keyword (xrange,center,range_unit) forwarded to the reader, which is how you keep this affordable on a big box.
Returns
(threads=..., times=..., speedup=..., best=..., sweet_spot=...). best is the fastest thread count, sweet_spot the smallest within 5% of it, and economical the smallest within 10%.
Prefer economical when the machine is shared or you have other snapshots waiting. A flat curve makes the 5% band arbitrary: on one measured run 8 threads missed it by half a second, so sweet_spot was 16, which costs twice the cores for five percent more speed.
reading_sweep(250, "/data/sim"; lmax=11)
reading_sweep(250, "/data/sim"; threads=[1,4,8,16], runs=1)Mera.run_merafile_benchmark — Function
run_merafile_benchmark(path, output, num_repeats=10)Time repeated reads of a compressed mera/JLD2 file and print the resulting statistics.
Used to produce the Mera-Files reading benchmark in the documentation. It reads the same output num_repeats times, so point it at a small dataset.
Mera.benchmark_conversion — Function
benchmark_conversion(path, output; merapath, components, runs=3, verbose=true)Measure what converting a snapshot to a MERA file costs, and how quickly that cost is repaid by faster re-reads.
Reads output from the RAMSES simulation in path, writes it out with savedata, then reads it back runs times. Reports the one-off conversion cost, the warm re-read time, and the break-even: the number of re-reads after which converting was worth it.
The break-even is the number to act on. Below it, read the RAMSES output directly. Above it, convert.
Keywords
merapath: where to write the MERA file. Defaults to a temporary directory, which is left in place so you can inspect or delete it.components: which of:hydro,:gravity,:particlesto include. Defaults to every component the snapshot actually has, so a run without gravity is fine.lmax, and any selection keyword (xrange,center,range_unit), forwarded to the readers. Without them the conversion reads the whole box, which on a large snapshot is slow and not comparable with a sweep that was capped.runs: how many times to read the MERA file back. The first is reported separately because it carries compilation; the warm figure is the minimum of the rest.verbose: print the report. The values are returned either way.
Returns
A NamedTuple with read_time, write_time, convert_total, first_read, warm_read, size_ramses, size_mera, size_ratio, breakeven and components.
# on your own simulation
benchmark_conversion("/path/to/simulation", 300)
# or on a public test simulation, to see what it reports
path = download_testdata("sedov3d_amr")
benchmark_conversion(path, 7)All requested components are held at once, so the peak is roughly the size of the snapshot in memory. On a large output, pass components=[:hydro] first.
See also: run_reading_benchmark, run_merafile_benchmark.
Mera.benchmark_report — Function
benchmark_report(path, output; merapath, components=[:hydro], lmax=missing,
runs=3, nfiles=64, outdir=homedir(), force=false, stages=:all)Run Mera's performance benchmarks on one snapshot and print a report that can be pasted straight into an issue, a paper, or the documentation.
Measures, in order:
- the snapshot itself, machine, filesystem,
ncpu, file count, size on disk - storage, IOPS and open/close cost across thread counts (
run_benchmark) - reading the RAMSES output, per component with GC share (
run_reading_benchmark) - conversion, what
savedatacosts and after how many re-reads it pays back (benchmark_conversion)
A high ncpu is the interesting case: the per-file parsing that a MERA file avoids is exactly what a snapshot with thousands of files makes expensive, so that is where the format advantage is largest.
Keywords
merapath: where to write the MERA file in step 4. Give it a real filesystem, not a small/tmp.components: which components to convert. Defaults to every component the snapshot has, so the comparison covers the whole dataset. Pass[:hydro]on a snapshot too large to hold all components in memory at once.lmax: cap the refinement level when reading. The honest way to benchmark a box too large to read whole.runs: repetitions per timed measurement.nfiles: how many files the throughput sweep samples. It reads file contents once per thread level per run, so this is deliberately bounded.outdir: where the report and the raw JSON/CSV go.max_threads: thread budget for every stage. When the:sweepstage runs and this is left at its default, the reading and conversion stages use the sweet spot the sweep found rather than the full budget, since that is the configuration the report goes on to recommend. Setting it explicitly pins every stage to your number instead. Defaults tomin(Threads.nthreads(), allocated_cpus()), so on a batch node it follows the scheduler's allocation rather than the machine's core count. Set it lower to leave headroom on a shared node.force: run a full read even when it looks too large for the machine's memory.stages::all, or any of:storage,:reading,:conversion,:sweepto run a subset.:sweepis never part of:all: it runs one full read per thread count per repetition, so it must be asked for by name,stages=[:storage, :sweep].
Returns
A NamedTuple with info, nfiles_total, bytes, filesystem, storage, reading, conversion and reportfile. Stages not run are nothing.
using Mera
benchmark_report("/data/sim/MilkyWay", 250; merapath="/data/merafiles")
# a box too large to read whole
benchmark_report("/data/sim/MilkyWay", 250; lmax=11, merapath="/data/merafiles")Every stage is capped at min(Threads.nthreads(), allocated_cpus()), where allocated_cpus reads SLURM_CPUS_PER_TASK and friends before falling back to Sys.CPU_THREADS. The storage sweep also stops at that number rather than climbing to 64. If Julia was started with more threads than the job owns, the environment log says so and names the right -t value.
Mera.benchmark_levels — Function
benchmark_levels(path, output; levels, runs=3, components=nothing,
max_threads=0, merapath, outdir, stages)Run benchmark_report once per refinement level and collect the results.
Reading cost, memory and the MERA-file advantage all depend strongly on how much of the AMR hierarchy you read, so a single level is one point on a curve. This measures the whole curve under identical conditions.
Every level runs in its own Julia process, so peak memory is measured cleanly rather than inherited from the previous level. A level that fails does not stop the sweep.
Keywords
levels: which levels to measure. Defaults to6:levelmax.runs: repeats per measurement inside each level.components: passed through; defaults to every component the snapshot has.max_threads: thread ceiling; defaults to the job's allocation.merapath,outdir: written per level intolmaxNNsubdirectories.stages: passed through tobenchmark_report.
Returns
A NamedTuple with levels, outdir, reports (the per-level report file paths that were produced) and failed.
using Mera
benchmark_levels("/path/to/simulation", 250;
merapath="/scratch/merafiles", outdir="/scratch/levels")
# a subset, and fewer repeats
benchmark_levels("/path/to/simulation", 250; levels=[6, 10, 13], runs=1)See also: benchmark_report, collect_levels.
Mera.collect_levels — Function
collect_levels(dir) -> VectorRead every MERA_BENCHMARK.txt under dir, print one row per refinement level, and write levels_summary.csv beside them.
Directories named superseded are skipped. They hold runs kept for the record but known to be unsound, and folding those into a summary is how a bad number gets published.
using Mera
benchmark_levels("/path/to/sim", 250; outdir="/scratch/levels")
collect_levels("/scratch/levels")See also: benchmark_levels, benchmark_report.
Mera.levelsplot — Function
levelsplot(reports; size=(1000, 700)) -> Makie.FigurePlot a whole collect_levels series against refinement level: read time for both paths, the speedup, memory allocated, and size on disk.
One level is a point; the series is the argument. It shows why a single speedup number is misleading: RAMSES read cost is dominated by parsing every file whatever you ask for, while the MERA path scales with what you actually requested, so the ratio grows as the request narrows.
Needs a Makie backend (using CairoMakie).
using Mera, CairoMakie
rs = collect_levels("/scratch/levels")
Makie.save("levels.png", levelsplot(rs))Mera.BenchmarkReport — Type
BenchmarkReportWhat benchmark_report returns. Fields: info, nfiles_total, bytes, storage_split (bytes per component), filesystem, storage, reading, conversion, sweep, work_threads, reportfile. Stages that were not run are nothing.
Mera.IOBenchmark — Type
IOBenchmarkResult of run_benchmark: the iops, throughput and openclose sub-results (each with .samples/.stats), the number of runs, the threads levels tested, and total_elapsed seconds. Pass it to plot_results for a figure.
Mera.benchmarkplot — Function
benchmarkplot(r; size=(1000, 760)) -> Makie.FigureTurn a benchmark_report result into one figure: the reading thread sweep, the read-time comparison, the memory each path churns through, and storage IOPS against thread count. Panels for stages that were not run are omitted.
Needs a Makie backend, so using CairoMakie first. The Figure is returned; save it with Makie.save("bench.png", fig). benchmark_report saves one automatically when a backend is already loaded.
using Mera, CairoMakie
r = benchmark_report("/data/sim", 250; stages=[:storage, :sweep, :conversion])
Makie.save("bench.png", benchmarkplot(r))Mera.filesystem_info — Function
filesystem_info(path) -> NamedTupleWhat kind of storage path sits on: type (lustre, gpfs, nfs, ext4, xfs, apfs, ...), mount point, and stripe for Lustre. Empty strings where it cannot be determined.
A read benchmark without this is not reproducible. A number from a Lustre scratch filesystem and the same number from a local NVMe describe different machines.
Mera.allocated_cpus — Function
allocated_cpus() -> IntHow many CPUs this process is actually entitled to, rather than how many the machine has. Reads the batch scheduler's own variables first (SLURM_CPUS_PER_TASK, SLURM_JOB_CPUS_PER_NODE, PBS_NP, NSLOTS, OMP_NUM_THREADS), then falls back to Sys.CPU_THREADS.
On a shared node Sys.CPU_THREADS reports the whole machine, so using it to size a benchmark takes cores that belong to other jobs and produces numbers shaped by the contention you caused.
Mera.benchmark_mera_io — Function
benchmark_mera_io(simulation_path::String, output_num::Int;
test_sizes=["32KB", "64KB", "128KB", "256KB"])Benchmark different I/O configurations to find optimal settings for your specific simulation.
This function tests various buffer sizes with your actual data to determine which configuration gives the best performance on your system.
Arguments
simulation_path: Path to your RAMSES simulation directoryoutput_num: Output number to test withtest_sizes: Array of buffer sizes to test (as strings)
Returns
- Dictionary with benchmark results and recommended optimal settings
Example
# Standard benchmark
results = benchmark_mera_io("/path/to/simulation", 300)
# Custom buffer sizes to test
results = benchmark_mera_io("/path/to/simulation", 300,
test_sizes=["64KB", "128KB", "256KB", "512KB"])
# Access results
optimal_buffer = results["optimal_buffer_size"]
performance_gain = results["performance_improvement"]What it does
- Tests each buffer size with your actual simulation data
- Measures getinfo() and gethydro() performance
- Identifies the optimal buffer size for your system
- Automatically applies the best settings
- Returns detailed performance comparison
Mera.benchmark_buffer_sizes — Function
benchmark_buffer_sizes(simulation_path::String, output_num::Int;
test_sizes=[32768, 65536, 131072, 262144], verbose=true)Benchmark different buffer sizes to find the optimal setting for this specific simulation.
I/O tuning
Buffer sizes and caching interact with thread count on a networked or slow filesystem; these are documented with the rest of the I/O controls in the Mera-Files API.
Related
- Multi-Threading: the guide, including what to expect from more threads and the measured numbers
- Performance: thread scaling for single- and multi-variable projections
- Run Your Own Benchmarks: read scaling
Every docstring in the package is also on the Complete API Reference.
Storage Benchmarks
Before committing to a long run it is worth knowing what the machine's storage actually delivers, which is often the limit rather than the CPU. run_benchmark measures IOPS, throughput and open/close cost across thread counts, and plot_results turns the result into a figure. The plotting method lives in a package extension, so it becomes available once a Makie backend is loaded:
using Mera, CairoMakie
results = run_benchmark("/path/to/simulation/folder"; runs=3)
fig = plot_results(results)Mera.run_benchmark — Function
run_benchmark(folder; runs=1, nfiles=64) → IOBenchmarkExecutes IOPS, throughput, and open/close tests. Returns all samples, stats, timings, and thread configurations as an IOBenchmark.
Point it at one snapshot directory. IOPS and open/close only open and close files, so they use the whole directory cheaply. The throughput test reads file contents once per thread level per run, so it samples nfiles files rather than the whole snapshot: on a large output, reading everything at every thread level would move hundreds of gigabytes. Raise nfiles for a larger sample if the storage can take it.
Mera.plot_results — Function
plot_results(res; bins=30) -> Makie.FigureVisualise a run_benchmark result as a 3-panel I/O figure: IOPS scaling vs threads, the per-file throughput distribution, and file open/close time vs threads. Needs a Makie backend (using CairoMakie or GLMakie); the Figure is returned, so save it with Makie.save("io.png", fig).
using Mera, CairoMakie
res = run_benchmark("/path/to/output_00250/"; runs=20) # benchmark your own data folder
fig = plot_results(res) # no download needed — built in
Makie.save("io_benchmark.png", fig)Structure-Finder Benchmarks
Mera.clumpfind_benchmarks — Function
clumpfind_benchmarks(gas; threshold, threshold_unit=:nH, linking_length=0.5, reps=5)Benchmark the structure finders on a loaded hydro object gas: each finder's wall time + clump count, the boundedness potentials (:approx/:direct/:tree), and a thread-scaling table for the per-clump stats path. Prints a report; returns a NamedTuple of the timings.