@@ -9,9 +9,12 @@ name: LAMMPS pair_matgl build
99# * gcc/g++/cmake/python3.
1010# If any of those are missing the workflow falls back to downloading them.
1111#
12- # Kokkos / GPU variant is *not* exercised here — GitHub-hosted runners have
13- # no GPU. Phase-3 hardware testing is delegated to a self-hosted CUDA
14- # runner (TODO).
12+ # A second job, build_pair_matgl_kokkos, exercises the Kokkos/GPU variant
13+ # (pair_style matgl/kk). It needs a self-hosted runner labeled
14+ # [self-hosted, gpu, cuda] with an NVIDIA driver + nvcc on PATH — there is
15+ # no such runner registered against this repo today, so the job will queue
16+ # forever until one exists. Verified manually on a single-GPU NERSC node;
17+ # not yet exercised by this workflow.
1518
1619on :
1720 push :
@@ -318,3 +321,282 @@ jobs:
318321 lammps_build/CMakeFiles/CMakeOutput.log
319322 lammps_build/CMakeFiles/CMakeError.log
320323 if-no-files-found : ignore
324+
325+ build_pair_matgl_kokkos :
326+ name : pair_matgl/kk (GPU, Kokkos)
327+ # Requires a self-hosted runner with an NVIDIA GPU, driver, and nvcc on
328+ # PATH, labeled to match below. No such runner is registered against
329+ # this repo yet, so this job will queue indefinitely until one exists.
330+ # `timeout-minutes` bounds that queue time instead of hanging forever.
331+ runs-on : [self-hosted, gpu, cuda]
332+ timeout-minutes : 60
333+
334+ env :
335+ LIBTORCH_VERSION : " 2.5.1"
336+ # libtorch CUDA wheel tag (e.g. cu118, cu121, cu124) — must match the
337+ # CUDA toolkit installed on the runner. Override via repo/org variable
338+ # CUDA_TAG if the runner ships a different CUDA version.
339+ CUDA_TAG : ${{ vars.CUDA_TAG || 'cu121' }}
340+ # Kokkos GPU architecture flag (see Kokkos_ARCH_* options) — must match
341+ # the runner's GPU. Override via repo/org variable KOKKOS_ARCH.
342+ KOKKOS_ARCH : ${{ vars.KOKKOS_ARCH || 'AMPERE80' }}
343+
344+ steps :
345+ - name : Checkout matgl
346+ uses : actions/checkout@v4
347+ with :
348+ path : matgl
349+
350+ - name : Probe runner for CUDA + LAMMPS source
351+ id : probe
352+ shell : bash
353+ run : |
354+ set -e
355+ if ! command -v nvidia-smi >/dev/null 2>&1; then
356+ echo "No NVIDIA driver found on this runner (nvidia-smi missing)" >&2
357+ exit 1
358+ fi
359+ nvidia-smi
360+ for cand in /opt/lammps /usr/local/src/lammps /lammps "$HOME/lammps"; do
361+ if [ -f "$cand/cmake/CMakeLists.txt" ]; then
362+ echo "lammps_src=$cand" >> "$GITHUB_OUTPUT"
363+ echo "Found LAMMPS source at: $cand"
364+ break
365+ fi
366+ done
367+ for cand in /opt/libtorch /usr/local/libtorch /libtorch "$HOME/libtorch"; do
368+ if [ -f "$cand/share/cmake/Torch/TorchConfig.cmake" ]; then
369+ echo "libtorch=$cand" >> "$GITHUB_OUTPUT"
370+ echo "Found libtorch at: $cand"
371+ break
372+ fi
373+ done
374+
375+ - name : Cache CUDA libtorch (fallback)
376+ if : steps.probe.outputs.libtorch == ''
377+ id : cache-libtorch
378+ uses : actions/cache@v4
379+ with :
380+ path : libtorch
381+ key : libtorch-${{ env.LIBTORCH_VERSION }}-${{ env.CUDA_TAG }}-cxx11abi
382+
383+ - name : Download CUDA libtorch (fallback)
384+ if : steps.probe.outputs.libtorch == '' && steps.cache-libtorch.outputs.cache-hit != 'true'
385+ shell : bash
386+ run : |
387+ curl -L -o libtorch.zip \
388+ "https://download.pytorch.org/libtorch/${CUDA_TAG}/libtorch-cxx11-abi-shared-with-deps-${LIBTORCH_VERSION}%2B${CUDA_TAG}.zip"
389+ unzip -q libtorch.zip
390+ rm libtorch.zip
391+
392+ - name : Resolve libtorch path
393+ id : libtorch
394+ shell : bash
395+ run : |
396+ if [ -n "${{ steps.probe.outputs.libtorch }}" ]; then
397+ echo "path=${{ steps.probe.outputs.libtorch }}" >> "$GITHUB_OUTPUT"
398+ else
399+ echo "path=${GITHUB_WORKSPACE}/libtorch" >> "$GITHUB_OUTPUT"
400+ fi
401+
402+ - name : Clone LAMMPS (fallback)
403+ # Vanilla LAMMPS source ships lib/kokkos/ (incl. bin/nvcc_wrapper)
404+ # in-tree — no separate Kokkos checkout needed.
405+ if : steps.probe.outputs.lammps_src == ''
406+ shell : bash
407+ run : |
408+ git clone --depth 1 --branch develop \
409+ https://github.com/lammps/lammps.git lammps_src
410+
411+ - name : Resolve LAMMPS source path
412+ id : lammps
413+ shell : bash
414+ run : |
415+ if [ -n "${{ steps.probe.outputs.lammps_src }}" ]; then
416+ echo "path=${{ steps.probe.outputs.lammps_src }}" >> "$GITHUB_OUTPUT"
417+ else
418+ echo "path=${GITHUB_WORKSPACE}/lammps_src" >> "$GITHUB_OUTPUT"
419+ fi
420+
421+ - name : Install Python deps (uv) for parity reference
422+ shell : bash
423+ run : |
424+ curl -LsSf https://astral.sh/uv/install.sh | sh
425+ export PATH="$HOME/.local/bin:$PATH"
426+ cd matgl
427+ uv venv --python 3.12
428+ uv pip install -e .
429+ uv pip install pytest
430+
431+ - name : Export tiny LAMMPS-loadable model + Python reference
432+ # Same fixture as the CPU job: a 4-atom Mo-S cell, tiny untrained
433+ # TensorNet. Export runs on CPU — no GPU needed to build the
434+ # TorchScript artifact, only to evaluate it inside LAMMPS later.
435+ shell : bash
436+ run : |
437+ export PATH="$HOME/.local/bin:$PATH"
438+ cd matgl
439+ mkdir -p ../lammps_artifacts
440+ uv run python - <<'PY'
441+ import json
442+ import numpy as np
443+ import torch
444+ from pymatgen.core import Lattice, Structure
445+ from pymatgen.optimization.neighbors import find_points_in_spheres
446+ from matgl.apps._pes_pyg import Potential
447+ from matgl.ext._lammps import LAMMPSMatGLModel
448+ from matgl.models._tensornet_pyg import TensorNet
449+
450+ torch.manual_seed(0)
451+ m = TensorNet(
452+ element_types=("Mo", "S"),
453+ is_intensive=False,
454+ units=16, nblocks=1, num_rbf=8,
455+ cutoff=4.0, use_warp=False, rbf_type="Gaussian",
456+ )
457+ p = Potential(model=m, calc_forces=True, calc_stresses=True)
458+ p.eval()
459+ w = LAMMPSMatGLModel(potential=p, dtype=torch.float32)
460+ w.eval()
461+ torch.jit.script(w).save("../lammps_artifacts/model.pt")
462+
463+ struct = Structure(
464+ Lattice.cubic(4.5),
465+ ["Mo", "S", "Mo", "S"],
466+ [[0, 0, 0], [0.5, 0.5, 0.5], [0.5, 0, 0.25], [0, 0.5, 0.75]],
467+ )
468+ src, dst, images, dist = find_points_in_spheres(
469+ struct.cart_coords, struct.cart_coords, r=4.0,
470+ pbc=np.array([1, 1, 1], dtype=np.int64),
471+ lattice=np.array(struct.lattice.matrix), tol=1e-8,
472+ )
473+ keep = (src != dst) | (dist > 1e-8)
474+ src, dst, images = src[keep], dst[keep], images[keep]
475+ pos = torch.tensor(struct.cart_coords, dtype=torch.float32)
476+ eidx = torch.tensor(np.stack([src, dst]), dtype=torch.long)
477+ ushifts = torch.tensor(images, dtype=torch.long)
478+ cell = torch.tensor(np.array(struct.lattice.matrix), dtype=torch.float32)
479+ z = torch.tensor([s.specie.Z for s in struct], dtype=torch.long)
480+ local = torch.ones(len(struct), dtype=torch.bool)
481+ out = w(pos, eidx, ushifts, cell, z, local, True)
482+ ref = {
483+ "energy": float(out["total_energy_local"].item()),
484+ "forces": out["forces"].detach().tolist(),
485+ }
486+ with open("../lammps_artifacts/reference.json", "w") as fh:
487+ json.dump(ref, fh, indent=2)
488+ print(json.dumps(ref, indent=2))
489+ PY
490+
491+ - name : Drop ML-MATGL-KOKKOS sources into LAMMPS src/KOKKOS/
492+ # src/KOKKOS/ is a *standard* LAMMPS package directory, always
493+ # scanned for PairStyle(...) macros when PKG_KOKKOS=ON — unlike
494+ # ML-MATGL, it doesn't need RegisterStyles() wired into
495+ # CMakeLists.txt by hand (that's only needed on the hand-patched
496+ # NERSC checkout this package was developed against). Dropping the
497+ # .cpp/.h straight in mirrors the CPU job's src/-root trick.
498+ shell : bash
499+ env :
500+ LAMMPS_SRC : ${{ steps.lammps.outputs.path }}
501+ run : |
502+ cp matgl/lammps/src/ML-MATGL/pair_matgl.cpp "${LAMMPS_SRC}/src/"
503+ cp matgl/lammps/src/ML-MATGL/pair_matgl.h "${LAMMPS_SRC}/src/"
504+ cp matgl/lammps/src/KOKKOS/pair_matgl_kokkos.cpp "${LAMMPS_SRC}/src/KOKKOS/"
505+ cp matgl/lammps/src/KOKKOS/pair_matgl_kokkos.h "${LAMMPS_SRC}/src/KOKKOS/"
506+ if ! grep -q ML-MATGL-KOKKOS-CI.cmake "${LAMMPS_SRC}/cmake/CMakeLists.txt"; then
507+ echo "include(${GITHUB_WORKSPACE}/matgl/lammps/cmake/ML-MATGL-KOKKOS-CI.cmake)" \
508+ >> "${LAMMPS_SRC}/cmake/CMakeLists.txt"
509+ fi
510+ # CI-only cmake fragment: just link Torch. The base pair_matgl.cpp
511+ # in src/ also needs Torch linked (same as the CPU job); the
512+ # Kokkos sources in src/KOKKOS/ are picked up automatically by
513+ # LAMMPS' own KOKKOS package glob once PKG_KOKKOS=ON.
514+ cat > matgl/lammps/cmake/ML-MATGL-KOKKOS-CI.cmake <<'CM'
515+ find_package(Torch REQUIRED)
516+ target_compile_features(lammps PRIVATE cxx_std_17)
517+ target_link_libraries(lammps PRIVATE ${TORCH_LIBRARIES})
518+ if(DEFINED TORCH_CXX_FLAGS)
519+ set_property(TARGET lammps APPEND_STRING
520+ PROPERTY COMPILE_FLAGS " ${TORCH_CXX_FLAGS}")
521+ endif()
522+ message(STATUS "ML-MATGL-KOKKOS (CI): linked against TORCH_LIBRARIES=${TORCH_LIBRARIES}")
523+ CM
524+
525+ - name : Configure LAMMPS (Kokkos/CUDA)
526+ shell : bash
527+ env :
528+ LAMMPS_SRC : ${{ steps.lammps.outputs.path }}
529+ LIBTORCH : ${{ steps.libtorch.outputs.path }}
530+ run : |
531+ BUILD_DIR="${GITHUB_WORKSPACE}/lammps_build"
532+ rm -rf "${BUILD_DIR}"
533+ cmake -B "${BUILD_DIR}" -S "${LAMMPS_SRC}/cmake" \
534+ -G Ninja \
535+ -D PKG_KOKKOS=ON \
536+ -D Kokkos_ENABLE_CUDA=ON \
537+ -D Kokkos_ENABLE_CUDA_LAMBDA=ON \
538+ -D Kokkos_ARCH_${KOKKOS_ARCH}=ON \
539+ -D CMAKE_CXX_COMPILER="${LAMMPS_SRC}/lib/kokkos/bin/nvcc_wrapper" \
540+ -D CMAKE_PREFIX_PATH="${LIBTORCH}" \
541+ -D MKL_INCLUDE_DIR=/usr/include \
542+ -D CMAKE_BUILD_TYPE=Release \
543+ -D BUILD_MPI=OFF
544+
545+ - name : Build LAMMPS
546+ shell : bash
547+ run : |
548+ cmake --build "${GITHUB_WORKSPACE}/lammps_build" -j 2
549+
550+ - name : Run pair_matgl/kk single-point deck on GPU
551+ # Same in.matgl_si deck as the CPU job, unmodified — `-sf kk`
552+ # dispatches `pair_style matgl` to `matgl/kk` automatically.
553+ shell : bash
554+ env :
555+ LIBTORCH : ${{ steps.libtorch.outputs.path }}
556+ run : |
557+ export LD_LIBRARY_PATH="${LIBTORCH}/lib:${LD_LIBRARY_PATH:-}"
558+ cp lammps_artifacts/model.pt matgl/lammps/tests/model.pt
559+ cd matgl/lammps/tests
560+ "${GITHUB_WORKSPACE}/lammps_build/lmp" -k on g 1 -sf kk -in in.matgl_si | tee log.lammps
561+
562+ - name : Diff LAMMPS energy against Python reference
563+ shell : bash
564+ run : |
565+ export PATH="$HOME/.local/bin:$PATH"
566+ cd matgl
567+ uv run python - <<'PY'
568+ import json
569+ import sys
570+
571+ ref = json.load(open("../lammps_artifacts/reference.json"))
572+ log = open("lammps/tests/log.lammps").read()
573+ # thermo_style for the test deck is "step pe fx fy fz pxx pyy pzz".
574+ # With ``run 0`` LAMMPS prints one numeric row; pull its second column.
575+ pe = None
576+ for line in log.splitlines():
577+ cols = line.split()
578+ if len(cols) >= 2 and cols[0].lstrip("-").isdigit():
579+ try:
580+ pe = float(cols[1])
581+ except ValueError:
582+ continue
583+ assert pe is not None, "Could not find PotEng row in log.lammps"
584+ ref_e = ref["energy"]
585+ diff = abs(pe - ref_e)
586+ print(f"LAMMPS PotEng = {pe!r}, Python ref = {ref_e!r}, diff = {diff:.3e}")
587+ if diff > 1e-3:
588+ print("ENERGY MISMATCH!", file=sys.stderr)
589+ sys.exit(1)
590+ PY
591+
592+ - name : Upload artifacts on failure
593+ if : failure()
594+ uses : actions/upload-artifact@v4
595+ with :
596+ name : lammps-kokkos-debug
597+ path : |
598+ matgl/lammps/tests/log.lammps
599+ lammps_artifacts/
600+ lammps_build/CMakeFiles/CMakeOutput.log
601+ lammps_build/CMakeFiles/CMakeError.log
602+ if-no-files-found : ignore
0 commit comments