SWATGenXSWATGenX
Watershed ExplorerExample modelsCloud calibrationDocsAccess
Sign inSign up

SWAT+ engine acceleration

Parallelizing the SWAT+ engine across CPU cores

An OpenMP wavefront over the routing DAG runs a watershed’s independent parts at once — byte-identical to stock SWAT+ at one thread, scaling to 5.33× at 24 threads on a 32-core node.

Measured on the 57,998-HRU Peace River benchmark on dedicated AWS instances. From the SWATGenX engine-acceleration study, submitted to Geoscientific Model Development (2026); open-source engine fork on GitHub.

  • Byte-identical at 1 thread
  • 5.33× at 24 threads (AWS c8a)
  • ~7.1× vs stock serial
  • 1.67–2.47× serial, no threads
Thread scaling — one simulated yearAWS c8a · 32 cores
0×2×4×6×ideal124816245.33×threads
One-year run: 143 s → 27 s. Self-relative speedup of the parallel binary; still climbing at 24 threads.

This page is the web companion to the SWATGenX engine-acceleration study — Accelerating a Regional Hyper-Resolution SWAT+ Model Without Changing the Science (submitted to Geoscientific Model Development, 2026). Every number below is measured on the 57,998-HRU Peace River benchmark on dedicated AWS instances and matches the manuscript; the open engine, with each optimization as an individually validated commit, is on the fork's main branch.

Watch: the routing wavefront explained

A 4-minute explainer of why watershed models are slow, why the channel network limits what can run in parallel, and how the wavefront unlocks it without touching the science.

1

Motivation

Can the SWAT+ engine use the cores a server already has?

SWAT+ runs one daily time step at a time, walking every object in the watershed — HRUs, routing units, channels, reservoirs, aquifers — in a single sequential order. For a small basin that is fine. For regional, hyper-resolution models with tens of thousands of HRUs it is the wall: a one-year run of the 57,998-HRU Peace River benchmark takes ~3 minutes on a fast modern core (and 5–6 on an older one), and a multi-decade calibration multiplies that by thousands of evaluations.

Modern servers have many idle cores. The question this page answers: can the SWAT+ engine itself use them — on one machine, shared memory, without changing the science — by running independent parts of the watershed at the same time?

2

Methods

A full-DAG wavefront over the routing network

Within one day, two objects can run concurrently only if neither is downstream of the other. The routing network is a directed acyclic graph (DAG): headwater HRUs feed routing units, which feed channels, which feed the next channel down to the outlet. We compute, for every object, its longest path from a headwater leaf — its level. Objects that share a level are mutually independent and run together; level L+1 waits for level L.

We then drive the daily step level-by-level under an OpenMP parallel loop. The land phase (tens of thousands of HRUs) is one enormous wide level; the channel network narrows as it converges on the main stem. Making this safe required auditing every shared 'current-object' scratch variable in the engine and giving each thread its own copy (threadprivate), so concurrent objects never clobber one another.

How we proved it correct

We validate by thread-count invariance: independent runs at different thread counts (and 1-thread vs the stock serial engine) must produce identical output to the last digit. Any run-to-run difference is, by definition, a real race — and points straight at the unprotected shared state. This turns correctness into a precise, automatable test, and it is what later exposed the one remaining race (in the particulate constituents) that a streamflow-only check would have missed.

ComponentPinWhat it is
Engine forkrafiei-vahid/swatplusswat-model/swatplus + OpenMP wavefront (-fiopenmp, ifx -O3 -ipo); every optimization a separate commit
Benchmark modelPeace HUC-8 0310010157,998 HRUs / 8,181 channels / 9,341 routing units — built by SWATGenX from NHDPlus HR + gSSURGO + PRISM
Hostsdedicated AWS EC2c8a / c8i / c5a .8xlarge — one model per box, thread-pinned (OMP_PROC_BIND=close), best of two

Table 2. Engine fork, benchmark model, and dedicated hosts used for the measurements on this page.

3

Results and discussion

The optimization journey: from I/O to CPU time

Making a very large SWAT+ model fast was not one fix but a sequence — each step removed the bottleneck the previous step exposed, and the dominant cost kept moving: from disk, to startup, to the land phase, to channel routing, and now to thread synchronization. The colour shows the class of each fix; none of them change the model's science.

Why it unlocks scaling: PSO calibration is capped at ~40 vCPU per model — with ≤40 parameters and good initial simulations, a larger population doesn't help (and can hurt). A parallel engine is the only way to put more than 40 cores on a single model, so one model's calibration can scale out to the whole cluster.

I/O
Algorithmic
Profiling
Parallel
Scheduling

1. NetCDF output backend

Bottleneck: Output I/O dominated — huge plain-text result files.

Fix: Binary NetCDF writer. Output size and write time cut sharply.

2. Filtered print

Bottleneck: Writing output rows nobody needed.

Fix: Emit only the requested objects/variables. Less I/O, smaller files.

3. O(1) startup name lookup (upstream PR #219)

Bottleneck: O(N²) string name-matching while reading inputs.

Fix: Hash name→index lookup in hru_read. Startup cost collapses on large models.

4. Per-row daily reset (upstream PR #220)

Bottleneck: Zeroing whole all-HRU arrays every day.

Fix: Reset only the active HRU's row. Output-identical; daily reset cost removed.

5. Profile the engine (Intel VTune)

Bottleneck: Where does a simulation actually spend time?

Fix: Hotspot + source-line profiling. Finding: the cost is overhead, not hydrology.

6. HRU land-phase wavefront

Bottleneck: 58k HRUs processed one at a time.

Fix: OpenMP parallel-do over a routing-DAG level. The wide land level runs concurrently.

7. Engine reentrancy (threadprivate)

Bottleneck: Shared 'current-object' globals raced under threads.

Fix: Give each thread its own copy; thread-count-invariance test. Land phase bit-identical across thread counts.

8. Channel parallelization (full-DAG wave)

Bottleneck: Routing still serial after the land phase.

Fix: Extend the wavefront to channels/reservoirs/units. Whole daily step parallel, level by level.

9. One parallel region per day

Bottleneck: Hundreds of levels × 365 days of thread fork/join.

Fix: Fork the team once per day, barrier per level. Eliminated the 4→8-thread regression.

10. ch_temp landscape-unit reset

Bottleneck: Re-zeroing the entire LSU array per channel per day (the single hottest line, ~731 s).

Fix: Reset only the units each channel uses. Output-identical; nearly free serially but shrinks the parallel critical path.

11. Fuse the width-1 barriers

Bottleneck: With channels cheap, threads spun at the per-level barriers — many on width-1 main-stem levels.

Fix: Run consecutive width-1 levels serially on one thread under a single barrier (no parallelism lost). Lifts the scaling ceiling a further ~7–11% at every thread count.

All four upstream pull requests (#213, #214, #219, #220) are open against swat-model/swatplus; the engine runs them today in our fork.

Two products: a serial gain for everyone, and thread scaling for one model

Peace River HUC-8, 57,998 HRUs, 1 simulated year, dedicated AWS c8a.8xlarge (AMD EPYC "Turin", 32 cores, SMT off) · thread scaling of the final engine, self-relative to its own 1-thread wall; ifx -O3 -ipo; daily channel_sd output; thread-pinned, best of two on a dedicated box.

Serial baseline
143 s
Best (24 threads)
27 s
Peak speedup
5.33×

Figure 1. Measured thread scaling of the final engine on the dedicated c8a box (AMD EPYC "Turin", 32 cores, SMT off), 57,998-HRU benchmark, one simulated year, full wavefront, self-relative to its own 1-thread wall. Bars are measured speedup; the dashed line is ideal linear scaling. The full wavefront reaches 5.33× at 24 threads; with the 1.67× serial gain on top, the end-to-end acceleration versus the stock single-core engine is ~7.1×.

ThreadsWall (s)SpeedupNotes
1143.11.00×serial reference (parallel binary, 1 thread)
284.11.70×
451.32.79×
836.23.95×
1629.24.90×
2426.95.33×best (32-core box)

Table 1. Wall-clock time and self-relative speedup by thread count (final engine, dedicated AWS c8a.8xlarge, one simulated year, ifx -O3 -ipo, full wavefront, daily channel_sd output, best of two).

The campaign yields two independent products. Group A — serial overhead removal (the same fixes that drive the I/O→CPU journey above) — needs no threads and changes no model behavior, so it accelerates every existing run on a single core: a cumulative 1.67× on this c8a node (the stock engine's one-year wall drops 191.9 s → 115.2 s), and larger on more memory-starved cores (up to 2.47× on the Intel "Granite Rapids" part). Group B — the routing-DAG wavefront — then scales a single model across cores.

On the dedicated 32-core c8a box the full wavefront scales to 5.33× at 24 threads (table and chart, self-relative to its own 1-thread wall). Multiplying the two layers, the end-to-end acceleration versus the stock single-core engine is ~7.1× — a one-year run drops from 191.9 s to 26.9 s. Two channel-side fixes got the scaling there: removing the dominant ch_temp serial hotspot (which shrank the Amdahl serial fraction and so lifted the achievable ceiling), then fusing the width-1 main-stem wave levels.

Scaling is bounded by physical cores and memory bandwidth, not thread count — this is a memory-bound model. The 32-core Turin keeps climbing to 24 threads; the 16-core Intel and AMD parts peak at their physical-core count and regress when threads spill onto SMT siblings. The full cross-hardware study on three dedicated cloud CPUs is below.

Cross-hardware scaling on dedicated cloud CPUs

The identical engine and the same one-simulated-year benchmark workload, thread-pinned (OMP_PROC_BIND=close, OMP_PLACES=cores), best of two, on three dedicated AWS instances. Speedup is each machine's own 1→N ratio on the parallel binary; the stock single-core wall and the Group-A serial gain are listed per machine so the two layers can be composed.

  • AMD EPYC — Zen 5 "Turin"32 cores · SMT off · stock 191.9 s · serial gain 1.67× · AWS c8a.8xlarge
  • Intel Xeon — "Granite Rapids"16 cores · SMT on · stock 272.2 s · serial gain 2.47× · AWS c8i.8xlarge
  • AMD EPYC — Zen 2 "Rome"16 cores · SMT on · stock 350.6 s · serial gain 2.16× · AWS c5a.8xlarge
Workersc8ac8ic5a
11.00×1.00×1.00×
21.70×1.59×1.62×
42.79×2.41×2.47×
83.95×3.29×3.21×
124.54×3.75×3.60×
164.90×4.01×3.81×
205.18×3.73×3.51×
245.33×3.74×3.58×

Values are speedup vs each machine's own 1 worker. ◁ marks each CPU's peak (beyond it, hyperthreads only contend).

AMD Turin (32 real cores, SMT off) is the scaling champion — still climbing at 24 threads (5.33×) and fastest in absolute wall time (a one-year run in 26.9 s). With the 1.67× serial gain on top, that is ~7.1× end-to-end versus the stock single-core engine.

Intel Granite Rapids has the fastest single core, so it wins the Group-A serial round (2.47×) — but it peaks at exactly 16 workers (its physical-core count, 4.01×) and REGRESSES at 20/24 as threads land on SMT siblings. The Rome box hits the same wall at its 16 cores (3.81×).

Speedup is bounded by physical cores and memory bandwidth, not thread count: this is a memory-bound model, so 32 real cores beat 16-cores-plus-SMT. Pushing past the physical-core count never helps and usually hurts.

The two layers are orthogonal and multiply: the serial gain is largest on the most memory-starved cores (2.47× on Intel), while the thread scaling is largest where there are the most real cores (5.33× on the 32-core AMD). The interactive calculator below composes them for any CPU/core/mode choice.

Estimate your speedup

Pick a CPU, a core count, and a correctness mode to estimate how much faster one engine run (e.g. one calibration evaluation) becomes versus the stock single-core engine. Every curve is directly measured on a dedicated AWS instance (one model per box, threads pinned, best of two runs) with the 57,998-HRU benchmark and daily channel_sd output. Speedup vs stock = stock single-thread wall / final-engine N-thread wall; serial and parallel gains compose multiplicatively.

CPU

c8a Turin 32c
c8i Granite Rapids 16c
c5a Rome 16c

Correctness mode

Full wavefront
Byte-identical

flow / sediment / dissolved bit-exact; ~1e-7 reorder on particulate WQ

Cores: 8

Serial engine (Group A)
1.67×
Parallel @ 8 cores
3.18×
Total vs stock 1-core
5.30×

Total = serial × parallel = stock single-core wall (192 s) ÷ final-engine 8-thread wall (36.2 s), both at the same daily channel_sd output scope. All three curves are directly measured on this CPU.

Total speedup (serial × parallel) versus the stock single-core engine, AWS c8a — AMD EPYC "Turin", 32 cores, full wavefront mode. Dashed line = ideal linear.

Where the time goes: an Intel VTune profile

An Intel VTune hotspots profile of the final parallel engine at 8 threads (180-day window) shows exactly why the ceiling is where it is — and that high CPU usage is not the same as useful work.

Peace River HUC-8 · final engine · 8 threads · 180-day window · Intel VTune hotspots

73%
25%
Effective (real work)929 s
Spin (idle at barriers)312 s
Overhead (scheduling, threadprivate)25 s

Figure 2. Intel VTune CPU-time breakdown of the final engine at 8 threads (Peace River, 180-day window): of ~1,267 CPU-seconds, 73% is effective work, 25% is spin (idle threads waiting at barriers), 2% is overhead. The effective work is dominated by serial channel routing (sd_channel_control3), ~7.7× the parallel land phase.

RoutineCPU time (s)Role
sd_channel_control3 (channel routing + water quality + sediment)698serial main stem
hru_control (the parallel land phase)90parallel
command_object / ru_control (dispatch + routing units)41mixed

Of the ~1,267 CPU-seconds the eight threads burned, only ~73% was real work — ~25% was threads spinning at barriers, idle, waiting for the serial main stem to finish. That spin is why a CPU monitor shows 80–90% utilization even though the per-thread payoff is bounded: busy cores are not the same as productive cores.

And the real work is lopsided: channel routing (sd_channel_control3, 698 s) costs about 7.7× the HRU land phase (hru_control, 90 s) — and it is the part that runs serially down the main stem. The land phase we parallelized is only ~12% of the compute, so even perfect HRU scaling can't move the total much. The next real lever is the channel network itself (overlapping its levels, or speeding the per-channel water-quality and sediment math), not more threads.

Acting on this, source-line profiling pinned the single hottest line in the entire engine: the stream-temperature routine (ch_temp) was re-zeroing the ENTIRE landscape-unit array — every routing unit in the watershed — once per channel, every day, even though each channel only reads its own few units. Resetting only the units a channel actually uses (an output-identical fix) barely changes the serial time but markedly raised the multi-thread ceiling, because the cut was on the serial main stem and shrank the Amdahl serial fraction. The methodology working: profile, remove overhead, re-measure.

Re-profiling the fixed build confirms the shift: ch_temp has dropped out of the hotspots entirely (its hottest line went from ~731 s to ~0.8 s), and no channel routine is dominant anymore. What now dominates is not computation but synchronization — with the heavy serial channel work removed, the worker threads finish the wide land-phase level quickly and then sit idle at the per-level barriers while the single-width main stem is walked. That barrier spin (~25% of CPU time at 8 threads) is the new frontier — the next gains come from smarter scheduling (task-dataflow over the routing network), not from shaving more arithmetic.

Correctness: byte-identical at one thread; model-equivalent above, with one localized race

How we check: thread-count invariance — exact byte-level diffs of the daily output files between independent runs at different thread counts, and 1-thread vs the stock serial engine. 'Bit-identical' means every value matched to the last digit; a record is counted as differing if it is off by more than 1e-6, a deliberately strict tripwire, not a measure of magnitude.

  • At 1 thread the parallel engine is byte-identical to the unmodified stock serial engine — verified bit-for-bit on a one-year run — so an exact reference run is always available. The wavefront activates only at 2+ threads, so single-thread keeps the original execution order exactly.
  • At higher thread counts the hydrology and dissolved constituents are model-equivalent: flow, sediment, nitrate (NO₃) and soluble-phosphorus basin loads agree to <2% (mostly <0.5%) — the residual is floating-point reassociation from the order in which upstream hydrographs are summed, not a change in the modeled physics.
  • The HRU land phase is bit-identical across thread counts — water balance (hru_wb), nutrient balance (hru_nb) and sediment losses (hru_ls) all match run-to-run.
  • A non-ipo build is bit-identical to the serial source; the ~0.008% residual under the production -ipo build is compiler reassociation, not a difference in physics.

The open item: a localized particulate race, and a byte-identical mode that sidesteps it

With in-stream water quality active over a multi-year run, thread-count invariance exposes a 15–20% divergence in exactly three PARTICULATE / sediment-associated species — organic-N, ammonia and sediment-P. Run-to-run variation confirms it is a genuine data race, not reassociation. The dissolved species (NO₃, soluble-P) and the entire HRU land phase are spared, which localizes the race to the channel in-stream constituent-transport path.

The root cause is the compiler, not the algorithm. ThreadSanitizer traces it not to a missing threadprivate — every per-object buffer is privatized — but to ifx allocating the temporaries of the 152-byte hyd_output derived type (operator results, function returns, argument copy-ins) in shared static module storage rather than on the per-thread stack, so concurrent channels clobber the same static temporary. It is invariant to every compiler control we tried, and a field-by-field source rewrite is byte-identical at one thread but does not remove it.

Because the race lives entirely in the routing phase, the engine offers a byte-identical mode: run the land phase in parallel and the routing phase serially (SWATPLUS_ROUTING_SERIAL=1). That is race-free and bit-reproducible — including in-stream water quality — to the floating-point-reassociation floor. The cost is the routing phase's parallelism: byte-identical mode scales ~2.5× at 8 threads versus the full wavefront's ~4× (about a 37% forfeit at 8 threads). So practitioners pick per run: byte-identical for in-stream water quality, the full wavefront (bit-exact for flow/sediment/dissolved species) otherwise.

The races we found and fixed

Thread-count-invariance testing surfaced several shared 'current-object' scratch variables that raced under the parallel wave. Each was given a per-thread copy:

HRU land phase — the big one

iwst, the current weather-station index, was shared: set per HRU then read two lines later as the weather source. Concurrent HRUs clobbered it in between, so a subset of HRUs read the wrong station — a ~0.2% drift in potential ET that rippled downstream. Privatizing it removed the largest source of non-determinism.

Plant & residue cycling

A cluster of 'temporary storage' scalars for residue decomposition, plant uptake, senescence, harvest and grazing (decomp, rsd_meta, pl_mass_up, leaf_drop, …) raced and perturbed biomass, which feeds soil evaporation.

Channel routing & sediment

Per-channel routing scratch (rttime, ben_area, rchdep, the rating-curve and sediment-budget buffers) and per-substep routing arrays were shared across channels running on the same level; each is now threadprivate or allocated per thread.

In-stream particulate constituents — the one that resisted

The last divergence (organic-N, ammonia, sediment-P) was NOT a missing threadprivate but the compiler placing hyd_output derived-type temporaries in shared static storage. It is handled by the byte-identical routing-serial mode rather than a source guard; a compiler-level fix is the remaining work.

A footnote: a pre-existing upstream bug, surfaced not introduced

Our bounds-checked debug build kept crashing in the stream-temperature routine. The cause turned out to be upstream, not ours: a channel whose first inflow is another channel reads array element hin_d(0), but the array is allocated from index 1. The production binary, built without bounds checking, silently reads the adjacent memory and continues — which is exactly why the water-temperature column is non-deterministic even in the stock engine.

Because stream temperature is output-only in this configuration (it feeds nothing in the water/sediment/nutrient balance), we left the behavior unchanged to match production, and flagged the bounds read for a separate upstream fix.

What remains

  • Fix the particulate-constituent race at its compiler root — move the hyd_output derived-type temporaries off shared static storage (a source-level or compiler change) so the full wavefront is byte-identical for in-stream water quality too, removing the ~37% byte-identical-mode forfeit.
  • Cut the barrier spin — now the dominant cost. With the channel-compute bottleneck removed, the wave's per-level barriers leave threads idle on the narrow deep levels. Schedule the routing network as a task-dataflow so a downstream channel starts the moment its own upstreams finish.
  • Mop up the smaller remaining compute: per-step string name-matching (replace obtyp comparisons with integer codes) and threadprivate-access overhead.
  • Confirm over a multi-decade run that the dissolved/flow reassociation residual stays bounded rather than slowly accumulating.
4

Conclusion

  • The SWAT+ engine can be parallelized on a single shared-memory node without changing the science — and at 1 thread it stays byte-identical to the stock production engine.
  • The campaign yields two orthogonal products: a machine-portable serial gain (1.67× on c8a, up to 2.47× on memory-starved cores) that accelerates every user on a single core with no behavior change, and thread parallelism that scales a single model to 5.33× at 24 cores — together ~7.1× end-to-end versus the stock single-core engine.
  • Scaling is bounded by physical cores and memory bandwidth, not thread count; the VTune profile shows serial channel routing costs ~7.7× the parallel land phase, so the main-stem routing — not more threads — is the remaining lever.
  • Correctness is validated by thread-count invariance: flow, sediment, nitrate and soluble-P are model-equivalent (<2%, mostly <0.5%); a localized, compiler-induced race in three particulate species (organic-N, ammonia, sediment-P) is handled by a byte-identical routing-serial mode, so in-stream water-quality runs stay bit-reproducible at a measured ~37% scaling cost.

FAQ

  • Has anyone made SWAT+ run in parallel for both HRUs and streams?

    Yes. SWATGenX built a shared-memory OpenMP parallelization of the SWAT+ engine that runs both the HRU land phase and the channel/routing network in parallel — an open fork of swat-model/swatplus at https://github.com/rafiei-vahid/swatplus (branch main). It parallelizes a single simulation on one machine (not just ensembles of independent runs) with a wavefront over the routing directed-acyclic graph: objects that share a dependency level are mutually independent and run concurrently. On a dedicated 32-core AWS c8a node the routing wavefront scales to 5.33× at 24 threads, and about 7.1× end-to-end versus the stock single-core engine. At one thread it is byte-identical to stock SWAT+. Caveats: production-scale builds use the Intel ifx compiler, and in the full-routing mode the N>1 particulate-constituent split carries a small floating-point round-off (a byte-identical land-parallel / serial-routing mode is available for in-stream water quality). Full methodology and benchmarks are in an engine-acceleration study submitted to Geoscientific Model Development (2026).

  • Is the parallel engine byte-identical to stock SWAT+?

    At one thread, yes — bit-for-bit identical to the unmodified stock serial engine on a one-year run, so an exact reference is always available. Above one thread the HRU land phase stays byte-identical across thread counts (water, nutrient, and sediment balances match exactly), and flow, sediment, nitrate, and soluble-phosphorus basin loads are model-equivalent (<2%, mostly <0.5%). The only divergence is three in-stream particulate constituents (organic-N, ammonia, sediment-P) under the full-routing wavefront, from a compiler-level data race — the land-parallel / serial-routing mode (SWATPLUS_ROUTING_SERIAL=1) is byte-reproducible including in-stream water quality.

  • How much faster is the parallel engine?

    Two independent layers. Serial overhead removal needs no threads and changes no results: 1.67× on the c8a reference node, up to 2.47× on more memory-starved CPUs (c8i Intel) and 2.16× on c5a — a free gain for every existing run. On top of that, the routing wavefront scales a single model to 5.33× at 24 threads on a dedicated 32-core AWS c8a node (3.95× at 8). Composed against the stock single-core engine on the same machine, a one-year run of the 57,998-HRU Peace River model drops from 191.9 s to 26.9 s — about 7.1× end-to-end. Note: that fastest ~7.1× configuration uses the opt-in parallel-routing wavefront, which trades exact bit-reproducibility for speed — a small round-off confined to the three in-stream particulate constituents (organic-N, ammonia, sediment-P), with all other loads model-equivalent (<2%, mostly <0.5%). The byte-identical guarantee applies to the HRU-parallel / serial-routing mode (SWATPLUS_ROUTING_SERIAL=1); it is not "~7× while byte-identical." All timings are on quiet, dedicated AWS instances, thread-pinned, best of two; we report no shared- or contended-host numbers.

  • How do I turn parallel routing on or off?

    Thread count is set with OMP_NUM_THREADS; at one thread the engine runs the original serial path and is byte-identical to stock. Routing parallelism is controlled by SWATPLUS_ROUTING_SERIAL: the production default keeps routing serial (the byte-identical, race-free land-parallel mode), and SWATPLUS_ROUTING_SERIAL=0 enables the full routing wavefront — fastest, bit-exact for flow/sediment/dissolved species, with a small round-off in the particulate constituents. So you pick per run: byte-identical for in-stream water quality, the full wavefront otherwise.

  • What compiler does the parallel engine need?

    The Intel Fortran compiler (ifx, -O3 -ipo; OpenMP via -fiopenmp). At regional hyper-resolution scale (tens of thousands of HRUs) gfortran is not reliable for these large models, so ifx is used for production-scale builds. The parallelization itself is standard OpenMP; the one open correctness item — a particulate-constituent race — traces to how ifx places 152-byte hyd_output derived-type temporaries in shared static storage rather than on the per-thread stack, and is sidestepped by the serial-routing byte-identical mode until a compiler-level fix lands.

  • How was correctness verified?

    By thread-count invariance: repeated runs at different thread counts, and 1-thread versus the stock serial engine, must produce identical output to the last digit. Any run-to-run difference is by definition a data race, which pinpoints the exact shared state to fix. This turned correctness into a precise, automatable test and surfaced every remaining race — including a shared weather-station index that caused a ~0.2% potential-ET drift on a subset of HRUs, and the localized particulate race that a streamflow-only check would have missed.

Related guides

SWAT+ performance profiling
SWAT+ runtime benchmark (measured on real models)
SWAT+ production engine
Methodology

Explore related

Last updated 2026-07-13.