Skip to content
Merged
324 changes: 324 additions & 0 deletions devops/scripts/benchmarks/benches/gromacs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,324 @@
# Copyright (C) 2025 Intel Corporation
# Part of the Unified-Runtime Project, under the Apache License v2.0 with LLVM Exceptions.
# See LICENSE.TXT
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

import os
import subprocess
import tarfile
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove the imports you don't use.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure

import urllib.request
from pathlib import Path
from .base import Suite, Benchmark
from options import options
from utils.utils import git_clone, download, run, create_build_path
from utils.result import Result


class GromacsBench(Suite):

def git_url(self):
return "https://gitlab.com/gromacs/gromacs.git"

def git_tag(self):
return "v2025.1"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Release today :)

Suggested change
return "v2025.1"
return "v2025.2"

Should not matter much, though.


def grappa_url(self):
return "https://zenodo.org/record/11234002/files/grappa-1.5k-6.1M_rc0.9.tar.gz"

def grappa_file(self):
return Path(os.path.basename(self.grappa_url()))

def __init__(self, directory):
self.directory = Path(directory).resolve()
model_path = str(self.directory / self.grappa_file()).replace(".tar.gz", "")
self.grappa_dir = Path(model_path)
build_path = create_build_path(self.directory, "gromacs-build")
self.gromacs_build_path = Path(build_path)
self.gromacs_src = self.directory / "gromacs-repo"

def name(self):
return "Gromacs Bench"

def benchmarks(self) -> list[Benchmark]:
models = [
"0001.5",
"0003",
"0006",
"0012",
"0024",
"0048",
"0096",
"0192",
"0384",
]
return [GromacsSystemBenchmark(self, model) for model in models]

def setup(self):
if not (self.gromacs_src).exists():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this check is unnecessary, it already happens inside of git_clone.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK

self.gromacs_src = git_clone(
self.directory,
"gromacs-repo",
self.git_url(),
self.git_tag(),
)
else:
if options.verbose:
print(f"GROMACS repository already exists at {self.gromacs_src}")

# Build GROMACS
run(
[
"cmake",
f"-S {str(self.directory)}/gromacs-repo",
f"-B {self.gromacs_build_path}",
f"-DCMAKE_BUILD_TYPE=Release",
f"-DCMAKE_CXX_COMPILER=clang++",
f"-DCMAKE_C_COMPILER=clang",
f"-DGMX_GPU=SYCL",
f"-DGMX_SYCL_ENABLE_GRAPHS=ON",

This comment was marked as resolved.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

f"-DGMX_FFT_LIBRARY=MKL",
f"-DGMX_BUILD_OWN_FFTW=ON",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Harmless but useless: we use MKL, so the question of building FFTW does not arise.

Suggested change
f"-DGMX_BUILD_OWN_FFTW=ON",

f"-DGMX_GPU_FFT_LIBRARY=MKL",
f"-DGMX_GPU_NB_CLUSTER_SIZE=8",
f"-DGMX_OPENMP=OFF",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For PVC we also should set DGMX_GPU_NB_NUM_CLUSTER_PER_CELL_X=1, see https://manual.gromacs.org/2025.1/install-guide/index.html#sycl-gpu-acceleration-for-intel-gpus.

Was that intentionally omitted because you want to reuse the same build across different Intel GPUS?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... We don't have a way of specifying gpu in the benchmarks right now. Before it was always PVC, but now we are also testing with BMG.

We probably need a method in utils or somewhere that will autodetect gpu type. Something like:

enum GpuTypes {
  INTEL_PVC,
  INTEL_BMG,
  OTHER // we can extend this with nvidia/amd gpus etc
  ...
}

options {
  gpu = OTHER
}

detect_gpu() {
  if options.sycl is None:
    return OTHER
  output = run(`sycl-ls --verbose`)
  default_gpu = re.search(output, ...);
  if default_gpu.contains(Data Center GPU Max):
    return PVC
  if default_gpu.contains(BMG?)
    return BMG

  return OTHER
}

somewhere in main:
options.gpu = detect_gpu()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like useful functionality to add, but probably scope creep for this PR. Could have a TODO comment about adding this in once the script can detect the device being targeted.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. For now I suggest we add -DGMX_GPU_NB_NUM_CLUSTER_PER_CELL_X=1 and leave a TODO to make it conditional later.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. For now I suggest we add -DGMX_GPU_NB_NUM_CLUSTER_PER_CELL_X=1 and leave a TODO to make it conditional later.

FYI, this flag is not critical either way. It improves the performance slightly on PVC, and is at least compatible with BMG (whether it improves the performance or not is an open question).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another minor suggestion is to add -DGMX_CYCLE_SUBCOUNTERS=ON for more detailed time breakdown in the log files; won't have much direct effect, but if there are any performance anomalies, the md.log files might become slightly more useful (if they are preserved).

],
add_sycl=True,
)
run(
f"cmake --build {self.gromacs_build_path} -j {options.build_jobs}",
add_sycl=True,
)
self.download_and_extract_grappa()

def download_and_extract_grappa(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use the existing utils.download

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove download_and_extract_grappa method and use utils.download instead for simplicity. If you find adding a verbose message is useful, please add it to utils.download

grappa_tar_file = self.directory / self.grappa_file()

if not grappa_tar_file.exists():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this isn't necessary, download already checks it.

model = download(
self.directory,
self.grappa_url(),
grappa_tar_file,
checksum="cc02be35ba85c8b044e47d097661dffa8bea57cdb3db8b5da5d01cdbc94fe6c8902652cfe05fb9da7f2af0698be283a2",
untar=True,
)
print(f"Grappa tar file downloaded and extracted to {model}")
else:
print(f"Grappa tar file already exists at {grappa_tar_file}")

def teardown(self):
pass


class GromacsSystemBenchmark(Benchmark):
def __init__(self, suite, model):
self.suite = suite
self.model = model # The model name (e.g., "0001.5")
self.gromacs_src = suite.gromacs_src
self.grappa_dir = suite.grappa_dir
self.gmx_path = suite.gromacs_build_path / "bin" / "gmx"

def name(self):
return f"gromacs-{self.model}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add "benchmark_type". Name needs to be unique.


def setup(self):
pass

def run(self, env_vars):
if not self.gmx_path.exists():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this check to setup() as you try running this binary there first without a check.

raise FileNotFoundError(f"gmx executable not found at {self.gmx_path}")

system_dir = self.grappa_dir / self.model

if not system_dir.exists():
raise FileNotFoundError(f"System directory not found: {system_dir}")

env_vars.update(
{
"LD_LIBRARY_PATH": str(self.grappa_dir)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

run takes an array of ld_library=[] that is then passed to one LD_LIBRARY_PATH.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK

+ os.pathsep
+ os.environ.get("LD_LIBRARY_PATH", ""),
"SYCL_CACHE_PERSISTENT": "1",
"GMX_CUDA_GRAPH": "1",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing this controls whether we use eager vs graph?

Can you make this configurable and add "eager"/"graph" variants of the benchmarks?

}
)

try:
# Generate configurations for RF
rf_grompp_result = run(
[
str(self.gmx_path),
"grompp",
"-f",
str(self.grappa_dir / "rf.mdp"),
"-c",
str(system_dir / "conf.gro"),
"-p",
str(system_dir / "topol.top"),
"-o",
str(system_dir / "rf.tpr"),
],
add_sycl=True,
)

# Run RF benchmark
rf_command = [
str(self.gmx_path),
"mdrun",
"-s",
str(system_dir / "rf.tpr"),
"-nb",
"gpu",
"-update",
"gpu",
"-bonded",
"gpu",
"-ntmpi",
"1",
"-ntomp",
"1",
"-nobackup",
"-noconfout",
"-nstlist",
"100",
"-pin",
"on",
]
rf_mdrun_result = run(
rf_command,
add_sycl=True,
)
rf_mdrun_result_output = rf_mdrun_result.stderr.decode()
rf_time = self._extract_execution_time(rf_mdrun_result_output, "RF")

print(f"[{self.name()}-RF] Time: {rf_time:.3f} seconds")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if verbose ?

benchmarks should not print anything.


# Generate configurations for PME
pme_grompp_result = run(
[
str(self.gmx_path),
"grompp",
"-f",
str(self.grappa_dir / "pme.mdp"),
"-c",
str(system_dir / "conf.gro"),
"-p",
str(system_dir / "topol.top"),
"-o",
str(system_dir / "pme.tpr"),
],
add_sycl=True,
)

# Run PME benchmark
pme_command = [
str(self.gmx_path),
"mdrun",
"-s",
str(system_dir / "pme.tpr"),
"-pme",
"gpu",
"-pmefft",
"gpu",
"-notunepme",
"-nb",
"gpu",
"-update",
"gpu",
"-bonded",
"gpu",
"-ntmpi",
"1",
"-ntomp",
"1",
"-nobackup",
"-noconfout",
"-nstlist",
"100",
"-pin",
"on",
]
pme_mdrun_result = run(
pme_command,
add_sycl=True,
)

pme_mdrun_result_output = pme_mdrun_result.stderr.decode()

pme_time = self._extract_execution_time(pme_mdrun_result_output, "PME")
print(f"[{self.name()}-PME] Time: {pme_time:.3f} seconds")

except subprocess.CalledProcessError as e:
print(f"Error during execution of {self.name()}: {e}")
raise

# Return results as a list of Result objects
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this comment is no longer correct

return [
self._result(
"RF",
rf_time,
rf_grompp_result,
rf_mdrun_result,
rf_command,
env_vars,
),
self._result(
"PME",
pme_time,
pme_grompp_result,
pme_mdrun_result,
pme_command,
env_vars,
),
]

def _result(self, label, time, gr_result, md_result, command, env_vars):
return Result(
label=f"{self.name()}-{label}",
value=time,
unit="s",
passed=(gr_result.returncode == 0 and md_result.returncode == 0),
command=" ".join(map(str, command)),
env=env_vars,
stdout=gr_result.stderr.decode() + md_result.stderr.decode(),
git_url=self.suite.git_url(),
git_hash=self.suite.git_tag(),
)

def _extract_execution_time(self, log_content, benchmark_type):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't bother passing the benchmark_type just so that you can print error message differently. The framework already collects errors into a map indexed by the benchmark name.

# Look for the line containing "Time:"
# and extract the first numeric value after it
time_lines = [line for line in log_content.splitlines() if "Time:" in line]

if len(time_lines) != 1:
raise ValueError(
f"Expected exactly 1 line containing 'Time:' in the log content for {benchmark_type}, "
f"but found {len(time_lines)}."
)

for part in time_lines[0].split():
if part.replace(".", "", 1).isdigit():
return float(part)

raise ValueError(
f"No numeric value found in the 'Time:' line for {benchmark_type}."
)

# def _extract_first_number(self, line):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove old code.

# parts = line.split()
# for part in parts:
# if part.replace(".", "", 1).isdigit():
# return float(part)
# return None

# def _parse_result(self, result, benchmark_type, execution_time):
# passed = result.returncode == 0
# return {
# "type": f"{self.name()}-{benchmark_type}",
# "passed": passed,
# "execution_time": execution_time,
# "output": result.stdout,
# "error": result.stderr if not passed else None,
# }

def teardown(self):
pass
2 changes: 1 addition & 1 deletion devops/scripts/benchmarks/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def create_run(self, name: str, results: list[Result]) -> BenchmarkRun:
github_repo = None

compute_runtime = (
options.compute_runtime_tag if options.build_compute_runtime else None
options.compute_runtime_tag if options.build_compute_runtime else "Unknown"
)

return BenchmarkRun(
Expand Down
11 changes: 0 additions & 11 deletions devops/scripts/benchmarks/html/data.js

This file was deleted.

3 changes: 3 additions & 0 deletions devops/scripts/benchmarks/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

from benches.compute import *
from benches.gromacs import GromacsBench
from benches.velocity import VelocityBench
from benches.syclbench import *
from benches.llamacpp import *
Expand Down Expand Up @@ -165,6 +166,7 @@ def main(directory, additional_env_vars, save_name, compare_names, filter):
SyclBench(directory),
LlamaCppBench(directory),
UMFSuite(directory),
GromacsBench(directory),
TestSuite(),
]

Expand Down Expand Up @@ -197,6 +199,7 @@ def main(directory, additional_env_vars, save_name, compare_names, filter):
except Exception as e:
failures[s.name()] = f"Suite setup failure: {e}"
print(f"{type(s).__name__} setup failed. Benchmarks won't be added.")
print(f"failed: {e}")
else:
print(f"{type(s).__name__} setup complete.")
benchmarks += suite_benchmarks
Expand Down
5 changes: 5 additions & 0 deletions devops/scripts/benchmarks/presets.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
presets: dict[str, list[str]] = {
"Full": [
"Compute Benchmarks",
"Gromacs Bench",
"llama.cpp bench",
"SYCL-Bench",
"Velocity Bench",
Expand All @@ -22,12 +23,16 @@
],
"Normal": [
"Compute Benchmarks",
"Gromacs Bench",
"llama.cpp bench",
"Velocity Bench",
],
"Test": [
"Test Suite",
],
"Gromacs": [
"Gromacs Bench",
],
}


Expand Down