Skip to content

Commit

Permalink
Merge pull request #34 from scal444/dev
Browse files Browse the repository at this point in the history
Merge v0.2 features
  • Loading branch information
scal444 authored Mar 21, 2021
2 parents 119dcc5 + 1cdea4b commit f7d2fd0
Show file tree
Hide file tree
Showing 22 changed files with 554 additions and 122 deletions.
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ sudo: false

language: python
python:
- "3.6"
- "3.7"
- "3.8"
- "3.9-dev"
- "3.9"
- "3.10-dev"

install:
- pip install codecov
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ yet another layer of complexity. Gromax is here to help.
### Via pip
To install the most recent release:
```
pip install git+https://github.com/scal444/gromax@v0.1.0
pip install git+https://github.com/scal444/gromax@v0.2.0
```

To install the current master version:
Expand All @@ -25,10 +25,8 @@ To install the current master version:
pip install git+https://github.com/scal444/gromax
```

TODO: Add branches as they come online

### Requirements
- python 3.6 or greater
- python 3.7 or greater

## Capabilities
- Given a Gromacs TPR file and a description of the hardware (CPU count and GPU IDs), generate a series of Gromacs run
Expand Down Expand Up @@ -56,6 +54,9 @@ in [the examples doc](docs/examples.md)!
- Feel free to file an issue bug/feature request, or create a PR. There is a
[known issues doc](docs/known_issues.md) for problems that are known but can't yet be addressed.

## Release notes
- Release notes can be found in [docs/releaes_notes](docs/release_notes)

## Other awesome resources
- Want to see how well your system scales to various clusters? Check out
[MDBenchmark](https://github.com/bio-phys/mdbenchmark)!
Expand Down
33 changes: 23 additions & 10 deletions admin/regenerate_reference_data.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,37 +14,50 @@ RUNBASE="generate --cpu_ids=0-3 --gpu_ids=0,1"
OUTDIR="$topdir/gromax/tests/integration/testdata"

# Basic 2016 test
$PYTHON "$exe" ${RUNBASE} --run_file="${OUTDIR}"/generate_test_default_2016.sh --gmx_version=2016
$PYTHON "$exe" ${RUNBASE} --run_file="${OUTDIR}"/generate_test_default_2016.sh --gmx_version=2016 \
--generate_exhaustive_combinations

# Basic 2018 test
$PYTHON "$exe" ${RUNBASE} --run_file="$OUTDIR"/generate_test_default_2018.sh --gmx_version=2018
$PYTHON "$exe" ${RUNBASE} --run_file="$OUTDIR"/generate_test_default_2018.sh --gmx_version=2018 \
--generate_exhaustive_combinations

# basic 2019 test
$PYTHON "$exe" ${RUNBASE} --run_file="$OUTDIR"/generate_test_default_2019.sh --gmx_version=2019
$PYTHON "$exe" ${RUNBASE} --run_file="$OUTDIR"/generate_test_default_2019.sh --gmx_version=2019 \
--generate_exhaustive_combinations

# basic 2020 test
$PYTHON "$exe" ${RUNBASE} --run_file="$OUTDIR"/generate_test_default_2020.sh --gmx_version=2020
$PYTHON "$exe" ${RUNBASE} --run_file="$OUTDIR"/generate_test_default_2020.sh --gmx_version=2020 \
--generate_exhaustive_combinations

# custom executable test
$PYTHON "$exe" ${RUNBASE} --run_file="$OUTDIR"/generate_test_custom_exe.sh --gmx_version=2016 \
--gmx_executable=/path/to/gmx_mpi
--generate_exhaustive_combinations --gmx_executable=/path/to/gmx_mpi

# custom trials test
$PYTHON "$exe" ${RUNBASE} --run_file="$OUTDIR"/generate_test_custom_ntrials.sh --gmx_version=2016 \
--trials_per_group=18
--generate_exhaustive_combinations --trials_per_group=18

# custom directory is ignored for generate
$PYTHON "$exe" ${RUNBASE} --run_file="$OUTDIR"/generate_test_default_2016.sh --gmx_version=2016 \
--directory=/path/to/nowhere
--generate_exhaustive_combinations --directory=/path/to/nowhere

# custom tpr
$PYTHON "$exe" ${RUNBASE} --run_file="$OUTDIR"/generate_test_custom_tpr.sh --gmx_version=2016 \
--tpr=custom_tpr.tpr
--generate_exhaustive_combinations --tpr=custom_tpr.tpr

# Test with strange CPU/GPU count that still works
$PYTHON "$exe" generate --cpu_ids=0-6 --gpu_ids=0,1 --run_file="$OUTDIR"/generate_test_odd_cpu_count.sh --gmx_version=2016
$PYTHON "$exe" generate --cpu_ids=0-6 --gpu_ids=0,1 --run_file="$OUTDIR"/generate_test_odd_cpu_count.sh \
--gmx_version=2016 --generate_exhaustive_combinations

# Test the single_sim_only argument
$PYTHON "$exe" ${RUNBASE} --run_file="$OUTDIR"/generate_test_single_sim_only.sh --gmx_version=2016 --single_sim_only
$PYTHON "$exe" ${RUNBASE} --run_file="$OUTDIR"/generate_test_single_sim_only.sh --gmx_version=2016 --single_sim_only \
--generate_exhaustive_combinations

# Test minimal subset only
$PYTHON "$exe" generate --cpu_ids=0-3 --gpu_ids=0 --run_file="$OUTDIR"/generate_test_minimal_subset.sh \
--gmx_version=2020

# Test minimal subset with single sim.
$PYTHON "$exe" generate --cpu_ids=0-3 --gpu_ids=0 --run_file="$OUTDIR"/generate_test_minimal_subset_single_sim.sh \
--gmx_version=2020 --single_sim_only
exit 0
14 changes: 14 additions & 0 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,20 @@ gromax generate --gmx_version=2020 --cpu_ids=0:2:7 --gpu_ids=0
gromax generate --gmx_version=2020 --cpu_ids=0:7 --gpu_ids=0 --single_sim_only
```

#### Exhaustive vs minimal configurations
Since Gromax v1.1, the `--generate_exhaustive_combinations` flag can be used to tune the number of configs Gromax
creates. It set to false by default, which adds the following limits:

* combinations of -pme and (where applicable to the Gromacs version) -bonded -update must be the same. E.g. a combination
such as -pme gpu -bonded cpu -update cpu is not created.
* A cap of 2 simultaneous simulations per GPU is set.

This significantly cuts down on the number of combinations generated, while not sacrificing much in coverage. Exhaustive
configs can still be generated by adding in the flag:
```shell script
gromax generate --gmx_version=2020 --cpu_ids=0:7 --gpu_ids=0 --generate_exhaustive_combinations
```

#### Some configuration options for the run script.
*NOTE: Each of these options can also easily be set in the first few lines of the benchmark script.*
```shell script
Expand Down
11 changes: 11 additions & 0 deletions docs/release_notes/release_notes_0.2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## Gromax v0.2.0
### Features
* Gromacs 2021 is now supported. Note that support of advanced GPU options is not yet implemented,
but any features that worked for Gromacs 2020 should work for Gromacs 2021.
* Reduced the default number of combinations generated. The default combinations
should still come close to maximizing performance. Exhausive benchmark generation
can still be requested using the `--generate_exhaustive_combinations` flag.
See [the examples doc](../examples.md) for more details.
### Compatibility
* Python 3.7 is now required.

72 changes: 49 additions & 23 deletions gromax/combination_generator.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import math

from dataclasses import dataclass

from gromax.constants import _SUPPORTED_GMX_VERSIONS
from gromax.hardware_config import HardwareConfig
from copy import deepcopy
from typing import List, Dict, Set, Any, Callable

# Constants
_SUPPORTED_VERSIONS: Set[str] = {"2016", "2018", "2019", "2020", "2021"}

# TODO: consolidate constants like this and potentially make configurable
_MAX_RANKS_PME_GPU = 8
from typing import List, Dict, Any, Callable

# Convenience definitions
# A grouping of GPU ids.
Expand All @@ -21,7 +18,17 @@
HardwareConfigBreakdown = List[HardwareConfig]


def genNtmpiOptions(total_procs: int, num_gpus: int, max_sims_per_gpu: int = 4) -> List[int]:
@dataclass
class GenerateOptions:
"""
Consolidated options for parameter combination generation.
"""
max_sims_per_gpu: int = 4
max_ranks_for_pme_gpu: int = 8
generate_exhaustive_options: bool = True


def genNtmpiOptions(total_procs: int, num_gpus: int, max_ranks_per_gpu: int = 4) -> List[int]:
"""
Breaks down the possible combinations of ntmpi for the given number of GPUs. For example, with
2 GPUs and 6 CPUs, the combinations are:
Expand All @@ -31,8 +38,6 @@ def genNtmpiOptions(total_procs: int, num_gpus: int, max_sims_per_gpu: int = 4)
Note that in this example ntmpi=3 does not work, because you can't eveny split 2 GPUs among
3 ranks.
Will not assign more than max_sims_per_gpu simulations to a single GPU
If passed with no GPUs, returns a size one list with the number of processors, as non-GPU simulations
should maximize thread counts.
"""
Expand All @@ -42,7 +47,7 @@ def genNtmpiOptions(total_procs: int, num_gpus: int, max_sims_per_gpu: int = 4)
return [total_procs]
curroption: int = num_gpus
options: List[int] = []
while curroption <= total_procs and math.ceil(curroption / num_gpus) <= max_sims_per_gpu:
while curroption <= total_procs and math.ceil(curroption / num_gpus) <= max_ranks_per_gpu:
if total_procs % curroption == 0 and curroption % num_gpus == 0:
options.append(curroption)
curroption += 1
Expand Down Expand Up @@ -135,7 +140,25 @@ def pruneOptionIf(parameters: List[ParameterSet], predicate: Callable[[Parameter
return [option for option in parameters if not predicate(option)]


def _createVersionedOptions(base_opts: ParameterSet, hw_config: HardwareConfig, gmx_version: str) -> ParameterSetGroup:
def _pruneUnlikelyCombinations(parameters: List[ParameterSet]) -> List[ParameterSet]:
"""
Removes combinations where PME != bonded != update - such as:
* PME GPU with bonded or update CPU
* PME CPU with bonded or update GPU
"""
def removalPredicate(params: ParameterSet) -> bool:
pme = params["pme"]
# For earlier versions without these options (eg. 2016),
# set the other options to PME so the inequality isn't triggered.
update = params.get("update", pme)
bonded = params.get("bonded", pme)
return pme != update or pme != bonded
return pruneOptionIf(parameters, removalPredicate)


def _createVersionedOptions(base_opts: ParameterSet, hw_config: HardwareConfig, gmx_version: str,
generate_options: GenerateOptions) -> ParameterSetGroup:
"""
Given a partial base parameter set, a hardware config, and a target Gromacs version, creates all of the
parameter combination possibilities.
Expand All @@ -161,12 +184,11 @@ def _createVersionedOptions(base_opts: ParameterSet, hw_config: HardwareConfig,

# Cap max ranks for PME GPU
def excessRanksPredicate(params: ParameterSet) -> bool:
return params["pme"] == "gpu" and params["ntmpi"] > _MAX_RANKS_PME_GPU
return params["pme"] == "gpu" and params["ntmpi"] > generate_options.max_ranks_for_pme_gpu
options = pruneOptionIf(options, excessRanksPredicate)

# set npme if more than one rank.
def nPmePredicate(params: ParameterSet) -> bool:
# TODO verify that this is correct on all versions
return params["pme"] == "gpu" and params["ntmpi"] > 1
options = applyOptionIf(options, "npme", 1, nPmePredicate)
if gmx_version >= "2019":
Expand All @@ -180,6 +202,8 @@ def nPmePredicate(params: ParameterSet) -> bool:
def multiRankPredicate(params: ParameterSet):
return params["pme"] == "cpu" and params["update"] == "gpu" and params["ntmpi"] > 1
options = pruneOptionIf(options, multiRankPredicate)
if not generate_options.generate_exhaustive_options:
options = _pruneUnlikelyCombinations(options)
# Add gputasks
for opt in options:
if opt.get("nb") == "gpu":
Expand All @@ -189,24 +213,25 @@ def multiRankPredicate(params: ParameterSet):


def _versionIsValid(version: str):
# TODO move this more central when sanitizing user input
return version in _SUPPORTED_VERSIONS
return version in _SUPPORTED_GMX_VERSIONS


def createRunOptionsForSingleConfig(hw_config: HardwareConfig, gmx_version: str) -> ParameterSetGroup:
def createRunOptionsForSingleConfig(hw_config: HardwareConfig, gmx_version: str,
generate_options: GenerateOptions) -> ParameterSetGroup:
"""
Generate the set of possible run options for a specific hardware config.
Note that this must be deterministic in order for a given number of CPUs and GPUs
(though the ordering can be arbitrary).
"""
options: ParameterSet = _createBaseOptions()
addConfigDependentOptions(options, hw_config)
return _createVersionedOptions(options, hw_config, gmx_version)
base_options: ParameterSet = _createBaseOptions()
addConfigDependentOptions(base_options, hw_config)
return _createVersionedOptions(base_options, hw_config, gmx_version, generate_options)


def createRunOptionsForConfigGroup(configs: HardwareConfigBreakdown, gmx_version: str) -> List[ParameterSetGroup]:
def createRunOptionsForConfigGroup(configs: HardwareConfigBreakdown, gmx_version: str,
generate_options: GenerateOptions) -> List[ParameterSetGroup]:
"""
Generate all the parameter combinations for a given subconfiguration of the hardware.
Expand All @@ -221,11 +246,12 @@ def createRunOptionsForConfigGroup(configs: HardwareConfigBreakdown, gmx_version

if not _versionIsValid(gmx_version):
raise ValueError("Invalid Gromacs version: {}. Supported options are {}".format(gmx_version,
_SUPPORTED_VERSIONS))
_SUPPORTED_GMX_VERSIONS))

# This gets us all the combinations we want, but with the wrong structure. The top level is for each partial
# hardware config, and the second level is over the options within each subconfig.
breakdowns_per_config: List[ParameterSetGroup] = [createRunOptionsForSingleConfig(config, gmx_version)
breakdowns_per_config: List[ParameterSetGroup] = [createRunOptionsForSingleConfig(config, gmx_version,
generate_options)
for config in configs]
# Now we reorder, inverting the organization such that
# [[subconfig1_option1, subconfig1_option2], [subconfig2_option1, subconfig2_option2]]
Expand Down
33 changes: 17 additions & 16 deletions gromax/command_line.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import argparse
import logging
from typing import List, Iterable

from gromax.constants import _SUPPORTED_GMX_VERSIONS, _GROMAX_VERSION
from gromax.utils import fatalError

# File constants.
_DESCRIPTION = "Gromax is a tool to build benchmarking scripts for Gromax and analyze the results. \n" \
"Generate scripts with the 'gromax generate' command, and analyze results with 'gromax analyze'."
Expand Down Expand Up @@ -51,12 +53,16 @@ def _buildParser() -> argparse.ArgumentParser:

generate_group = parser.add_argument_group("generate", "arguments for 'gromax generate'")
generate_group.add_argument('--gmx_version', type=str, metavar="",
help='Gromacs version - "2016", "2018", "2019", "2020", or "2021"', )
help=f'Gromacs version - one of {sorted(set(_SUPPORTED_GMX_VERSIONS))}', )
generate_group.add_argument("--run_file", type=str, help="Path to bash benchmark script to create.",
default="benchmark.sh", metavar="")
generate_group.add_argument("--gmx_executable", type=str, default="gmx", metavar="", help=(
"gmx or gmx_mpi executable path. Defaults to 'gmx', which works if the executable is in your path."
))
generate_group.add_argument("--generate_exhaustive_combinations", default=False, action="store_true",
help=("If set, Gromax will generate all possible combinations of PME/bonded/update "
"CPU/GPU options. If not set(by default), only options likely to have maximum "
"performance are generated."))
generate_group.add_argument("--trials_per_group", type=int, default=3, metavar="",
help="Number of times to run each parameter set.")
generate_group.add_argument("--tpr", type=str, help="Absolute path to the tpr file to benchmark.", metavar="")
Expand All @@ -72,39 +78,34 @@ def _buildParser() -> argparse.ArgumentParser:
help="If set, do not divide the hardware among multiple concurrent simulations")
analyze_group = parser.add_argument_group("analyze", "arguments for 'gromax analyze'")
analyze_group.add_argument("--directory", type=str, help="Path to execution/analysis directory.", metavar="")
parser.add_argument("--version", action="version", version="alpha")
parser.add_argument("--version", action="version", version=_GROMAX_VERSION)
parser.add_argument("--log_level", type=str, default="info", metavar="",
help="Set logging verbosity - 'silent', 'info'(default), or 'debug'")
return parser


def _failWithError(err: str):
logging.getLogger("gromax").error(err)
raise SystemExit(1)


def _checkGenerateArgs(args: argparse.Namespace) -> None:
good_versions: Iterable[str] = ("2016", "2018", "2019", "2020", "2021")
if args.gmx_version not in good_versions:
_failWithError("Invalid gmx version {}, must be one of {}".format(args.gmx_version, good_versions))
if args.gmx_version not in _SUPPORTED_GMX_VERSIONS:
fatalError("Invalid gmx version {}, must be one of {}".format(args.gmx_version,
sorted(_SUPPORTED_GMX_VERSIONS)))
if not args.cpu_ids and not args.num_cpus:
_failWithError("One of --cpu_ids or --num_cpus is required")
fatalError("One of --cpu_ids or --num_cpus is required")
if args.num_cpus:
if args.cpu_ids:
_failWithError("Cannot specify both --cpu_ids and --num_cpus")
fatalError("Cannot specify both --cpu_ids and --num_cpus")
args.cpu_ids = ",".join([str(identifier) for identifier in range(args.num_cpus)])
if not args.gpu_ids and not args.num_gpus:
_failWithError("One of --gpu_ids or --num_gpus is required")
fatalError("One of --gpu_ids or --num_gpus is required")
if args.num_gpus:
if args.gpu_ids:
_failWithError("Cannot specify both --gpu_ids and --num_gpus")
fatalError("Cannot specify both --gpu_ids and --num_gpus")
args.gpu_ids = ",".join([str(identifier) for identifier in range(args.num_gpus)])


def checkArgs(args: argparse.Namespace) -> None:
good_modes: Iterable[str] = ("generate", "execute", "analyze")
if args.mode not in good_modes:
_failWithError("'mode' is a required positional argument - options are 'generate', 'execute', 'analyze'")
fatalError("'mode' is a required positional argument - options are 'generate', 'execute', 'analyze'")

if args.mode == "generate":
_checkGenerateArgs(args)
Expand Down
7 changes: 7 additions & 0 deletions gromax/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from typing import FrozenSet
"""
Contains program-level constants
"""
_GROMAX_VERSION = "1.2-dev"

_SUPPORTED_GMX_VERSIONS: FrozenSet[str] = frozenset({"2016", "2018", "2019", "2020", "2021"})
2 changes: 1 addition & 1 deletion gromax/file_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def parseDirectoryStructure(directory: str) -> allDirectoryContent:
return result


def _maxItems(content: allDirectoryContent):
def _maxItems(content: Dict):
"""
Given a nested dictionary, determine the max number of items in the first subdictionary. So given,
{
Expand Down
Loading

0 comments on commit f7d2fd0

Please sign in to comment.