What actually breaks when you port a chemistry stack to DGX Spark
GB10 / Grace Blackwell / CUDA 13 / sm_121
October 9, 2026
The core GPU and quantum compute path of the NovoMCP engine runs on a single NVIDIA DGX Spark (GB10): MACE and ANI-2x neural-network potentials, the NVIDIA ALCHEMI toolkit, GROMACS molecular dynamics, AutoDock-GPU, xTB/CREST, and sTDA excited states. Not the CPU subset, and not literally everything, since structure prediction and some generative and free-energy paths are not in this port. But the compute core runs end to end, with no service calls leaving the box during a run.
The surprising part is not that it runs. It is why the hard parts were hard.
Provenance. Every number below was measured on NVIDIA GB10 (DGX Spark): CUDA 13.0, Ubuntu with the 6.17-nvidia kernel, 20 cores (10x Cortex-X925 plus 10x Cortex-A725), 128 GB unified memory. The MACE validation and the water benchmark were reproduced on a second, independent GB10 (drivers 580.14x and 580.173.02), the benchmark over three runs per size. A retail Spark's DGX-OS may differ, so pin your own versions beside any number you reuse.
the finding
On Grace, almost nothing that looks like an architecture problem actually is one. I misdiagnosed it three times:
- MACE crashed in a way that looked like a cuequivariance or sm_121 kernel bug. It was two allocator issues: an unbounded-OpenMP heap corruption at
import torch, and a glibc-allocator interaction in e3nn's GPU forward pass. Both cleared by environment variables. Neither was a CUDA or kernel fault. - GROMACS looked like it would not build for aarch64. It was a GROMACS by CUDA-13 version issue. The 2025.x line fails to link its PME-GPU kernels; the 2026.x line builds clean. Nothing to do with the architecture.
- sTDA (excited-state tools) looked like legacy Fortran that would not compile on Arm. It was three packaging bugs: the built binary is named
std2and notstda, a shared library was not on the path, and a parameter environment variable was unset. The Fortran had compiled the whole time.
The gap between "resolves for arm64" and "runs on Grace" was a handful of environment variables and packaging fixes. No rebuild, no source-built PyTorch. Stock CUDA-13 wheels ship sm_120 kernels and they forward-compat to GB10's sm_121.
The pattern worth taking away: on this hardware, reach for the environment, packaging, or version explanation before reaching for "the architecture is not supported." Three times running, the frightening diagnosis was the wrong one.
who this is for
If you have datacenter GPUs and a working pipeline on them, this port buys you nothing on throughput. A GB10 runs the benchmark below at roughly 0.4x an L40S. Stay where you are.
It is worth reading if you are in one of two situations. Either you are about to port a scientific Python and CUDA stack to Grace and would rather not lose a day to an allocator crash that points nowhere near the allocator, in which case skip to the rough edges and the recipe. Or you need chemistry compute that physically does not leave a box, on a desk, at about 60 W, and you are willing to trade roughly 2.5x on wall clock to get it.
the port table
validated = output matches a known reference. executed = ran to completion with plausible output.
| Component | aarch64 / sm_121 route | Status | Notes |
|---|---|---|---|
| PyTorch (cu130) | stock wheel; sm_120 kernels forward-compat to sm_121 | validated | FP32 and FP64 matmul match CPU on sm_121 (max err 1.3e-4 / 4e-13); needs OMP_NUM_THREADS bounded |
| MACE-MPA-0 (ASE engine) | stock mace-torch (e3nn) | validated | ethanol float64 single point -46.2554 eV, exact to the reference (0.00 meV) |
| MACE-MPA-0 (ALCHEMI batched) | nvalchemi-toolkit[cu13] | validated | GPU-FIRE relax matches the ASE engine to 0.007 meV on the same geometry; needs jemalloc |
| ANI-2x | torchani | executed | ethanol -154.94 Ha plus forces |
| GROMACS-MD | 2026.x source; sm_121 plus ARM_NEON_ASIMD | executed | GPU MD, with nonbonded, PME and update all on GPU |
| AutoDock-GPU | source; make DEVICE=CUDA TARGETS=121 | executed | native sm_121 kernels; protein-ligand docking runs on the GPU |
| xTB / CREST | conda-forge linux-aarch64 | executed | GFN2 optimization converged (-10.498 Eh); CREST 3.0.2 |
| sTDA / xtb4stda | source build (meson plus openblas) | executed | excited states end to end; formaldehyde S1 n to pi* at 4.33 eV / 286 nm |
| AmberTools | conda-forge linux-aarch64 | installed | present, not separately benchmarked |
| DGL (GNN embeddings) | dgl 2.2.0 aarch64 | validated | GIN embeddings match x86 to about 1e-6 (sum and L2, CCO and aspirin) |
One note that saves money later: xTB, CREST, and AmberTools come from conda-forge's linux-aarch64 channel, not PyPI. The PyPI builds are x86 only.
the two Grace rough edges
Both live in the stock CUDA-13 aarch64 stack rather than in user code, and both are runtime failures. They only appear when the code actually runs on silicon. A build-only or dependency-resolution check on emulated aarch64, which is what most CI does, exercises neither. A port that stops at "it builds for arm64" catches neither.
1. import torch heap-corrupts unless OMP_NUM_THREADS is bounded. The wheel's bundled libnvpl_lapack_lp64_gomp and libarm_compute spawn unbounded OpenMP threads at static-init on the 20-core chip and corrupt the allocator (malloc(): corrupted top size) before any user code runs. numpy imports fine, so it is torch's bundled libraries specifically. Any bounded OMP_NUM_THREADS fixes it. gdb confirmed the abort is in the allocator during C-extension load.
2. e3nn's GPU forward pass trips a glibc-allocator corruption. free(): corrupted unsorted chunks during the MACE forward pass, nondeterministic: a single point sometimes survives, a multi-step relax reliably trips it. LD_PRELOAD=libjemalloc.so.2 clears it. This is the non-obvious one, because the error points nowhere near the allocator, so swapping the allocator is not where you would look for a crash in a CUDA stack.
For GROMACS specifically, the 2025.x line fails to link its PME GPU kernels against CUDA 13 (undefined reference to pme_solve_kernel). Nonbonded compiles, PME does not. 2026.x builds clean. Again, a version boundary rather than the architecture.
the recipe, distilled
- PyTorch stack: stock cu130 wheels,
OMP_NUM_THREADS=<n>,LD_PRELOAD=libjemalloc.so.2. No source-built torch. - Source-compiled CUDA (GROMACS, AutoDock): set the arch flag explicitly (
sm_121,TARGETS=121). GROMACS needs the 2026.x line. - CPU chemistry tools (xTB, CREST, AmberTools): conda-forge
linux-aarch64. - sTDA: builds from source on aarch64 with meson and openblas. The three gotchas are all packaging. Install the built
std2binary under the namestda, put itslibcint.soon the library path, and pointXTB4STDAHOMEat the parameter files, since it does not expand~.
measured limits
GB10 is consumer-derived Blackwell, and it shows where you would expect.
- FP64 about 0.43 TFLOP/s against FP32 about 14.6 TFLOP/s on an 8192 cubed matmul, a penalty of roughly 34x. Keep double-precision paths, including some QM and MACE float64 geometry optimization, on the CPU, or run them in float32 on the GPU. This is the number most likely to surprise someone porting an FP64-heavy code.
- Memory bandwidth about 227 GB/s measured on a 2 GiB device-to-device copy, roughly 82% of GB10's 273 GB/s spec. Spec to spec, that 273 GB/s is about 5.7x below an A100's 1,555 GB/s, and MD is bandwidth-bound, so that is where the gap lives.
- 128 GB unified memory, addressed coherently by the GPU, with no host to device copy.
GB10 against a datacenter GPU: efficient, not fastest
I ran the same GROMACS SPC/E water benchmark on one GB10 and on an NVIDIA L40S (48 GB, a current datacenter card), with the same GROMACS 2026.3, the same mdp, cutoffs, step counts and water model, PME at 2 fs, nonbonded, PME and update all on the GPU, after energy minimization.
| Atoms | GB10 ns/day | L40S ns/day |
|---|---|---|
| 98,319 | 220.3 ± 3.1 | 549 |
| 975,357 | 19.21 ± 0.02 | 54.1 |
| 3,875,106 | 4.244 ± 0.008 | 10.5 |
| 28,307,433 | 0.28 | 0.67 |
Throughput per atom stays roughly flat from 100 K to 4 M atoms, bandwidth and PME bound as expected. GB10 runs this workload at roughly 0.4x the L40S's throughput, about 2.5x slower, consistently across sizes, with GB10 figures the mean of three runs per size at sd under 2%.
Where it turns interesting is power. I measured both cards at the GPU rail, host-sampled with nvidia-smi during each run. GB10 drew 56 to 66 W sustained, peaking around 70 W. The L40S drew about 190 to 215 W sustained, peaking near 300 W, well under its 350 W TDP.
On measured, like-for-like power, GB10's throughput-per-watt edge works out to about 1.2 to 1.5x across sizes. That is real but modest, and notably not the roughly 2x a TDP-based estimate would have implied. The cleaner fact is absolute draw: GB10 runs this science at roughly a third of the L40S's GPU power, about 60 W against about 200 W, at about 2.5x slower, in a desk-side box with no service calls leaving it.
A note on memory. The 28 M atom system does not exceed a 48 GB card's VRAM; the L40S ran it on about 11 GB. GROMACS keeps the GPU-resident working set small, around 0.5 GB per million atoms, and the larger memory footprint on GB10 is consumed host-side during setup rather than on the GPU. The 128 GB unified pool is a real asset for workloads whose GPU footprint is large. This MD is not one of them, so there is no memory advantage to claim here.
the compute core, running locally
The point of a port table is what it enables. On one box, with no service calls leaving it during the run:
- A full protein-ligand molecular dynamics run: a 38,158-atom solvated complex of protein, ligand and water, energy-minimized, equilibrated, and run as production MD on the GPU, with trajectory and MM-GBSA analysis.
- The engine orchestrating neural-network relaxation (MACE on GPU), a quantum single point (GFN2-xTB), and GPU docking as routed tool calls. Generation through simulation, locally.
- The part I did not expect to get working: a fully local autonomous run. Pointed at a local LLM served on the same GB10 through Ollama, the engine's agent loop drove a task end to end. The model called the neural-network potential on its own and the GPU returned a relaxed ethanol energy of -46.244 eV, consistent with the MACE-MPA-0 numbers above, though a different relaxation path and so not identical to the 0.007 meV same-geometry figure. Model and compute on one box, no API key, no network calls. A 7B model handles a focused task reliably. The full multi-stage protocol is beyond it.
One finding from the larger agent runs belongs here, because it is a result about agentic chemistry rather than a blemish. A cloud-model agent ran a materials-screening protocol correctly, with every tool call valid and missing services reported honestly, and still drew a weak conclusion: it compared raw total energies across different molecules, which is meaningless. The orchestration was sound. The scientific judgment was not. The only thing that catches that is the recorded audit trail, which is why every run here is logged. An autonomous funnel without one is a confident answer you cannot check.
licensing note
The potential used throughout is MACE-MPA-0, which is MIT-licensed and safe for commercial use. It is not the same as MACE-OFF23, whose weights are released under a non-commercial and academic license. If you adapt this recipe for commercial work, confirm you are on an MIT checkpoint (MPA-0) rather than an OFF23 one.
standing on open source
None of this is mine alone: MACE, the NVIDIA ALCHEMI toolkit, GROMACS, AutoDock-GPU, xTB/CREST, ANI-2x, DGL, and the rest. What I added is the port table above and the two Grace bugs, written up so the next person running a scientific stack on a Spark finds the environment-variable fix before they lose a day to the allocator.
Measured on one GB10. "Executed" is not reference-validated on every row, and the FP64 penalty is real, so keep double precision off the hot path.