Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ci[cartesian]: Thread safe parallel stencil tests #1849

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def test_cartesian(
markers = " and ".join(codegen_settings["markers"] + device_settings["markers"])

session.run(
*f"pytest --cache-clear -sv -n {num_processes}".split(),
*f"pytest --cache-clear -sv -n {num_processes} --dist loadgroup".split(),
*("-m", f"{markers}"),
str(pathlib.Path("tests") / "cartesian_tests"),
*session.posargs,
Expand Down
56 changes: 35 additions & 21 deletions src/gt4py/cartesian/testing/suites.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ def get_globals_combinations(dtypes):
generation_strategy=composite_strategy_factory(
d, generation_strategy_factories
),
implementations=[],
implementation=None,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This might have been different in the past. The way we cache implementations now, there's only ever max one implementation per test context.

test_id=len(cls_dict["tests"]),
definition=annotate_function(
function=cls_dict["definition"],
Expand Down Expand Up @@ -199,14 +199,19 @@ def hyp_wrapper(test_hyp, hypothesis_data):

for test in cls_dict["tests"]:
if test["suite"] == cls_name:
marks = test["marks"]
if gt4pyc.backend.from_name(test["backend"]).storage_info["device"] == "gpu":
marks.append(pytest.mark.requires_gpu)
name = test["backend"]
name += "".join(f"_{key}_{value}" for key, value in test["constants"].items())
name += "".join(
"_{}_{}".format(key, value.name) for key, value in test["dtypes"].items()
)

marks = test["marks"].copy()
if gt4pyc.backend.from_name(test["backend"]).storage_info["device"] == "gpu":
marks.append(pytest.mark.requires_gpu)
# Run generation and implementation tests in the same group to ensure
# (thread-) safe parallelization of stencil tests.
marks.append(pytest.mark.xdist_group(name=f"{cls_name}_{name}"))

param = pytest.param(test, marks=marks, id=name)
pytest_params.append(param)

Expand All @@ -228,14 +233,19 @@ def hyp_wrapper(test_hyp, hypothesis_data):
runtime_pytest_params = []
for test in cls_dict["tests"]:
if test["suite"] == cls_name:
marks = test["marks"]
if gt4pyc.backend.from_name(test["backend"]).storage_info["device"] == "gpu":
marks.append(pytest.mark.requires_gpu)
name = test["backend"]
name += "".join(f"_{key}_{value}" for key, value in test["constants"].items())
name += "".join(
"_{}_{}".format(key, value.name) for key, value in test["dtypes"].items()
)

marks = test["marks"].copy()
if gt4pyc.backend.from_name(test["backend"]).storage_info["device"] == "gpu":
marks.append(pytest.mark.requires_gpu)
# Run generation and implementation tests in the same group to ensure
# (thread-) safe parallelization of stencil tests.
marks.append(pytest.mark.xdist_group(name=f"{cls_name}_{name}"))

runtime_pytest_params.append(
pytest.param(
test,
Expand Down Expand Up @@ -434,8 +444,11 @@ class StencilTestSuite(metaclass=SuiteMeta):
def _test_generation(cls, test, externals_dict):
"""Test source code generation for all *backends* and *stencil suites*.

The generated implementations are cached in a :class:`utils.ImplementationsDB`
instance, to avoid duplication of (potentially expensive) compilations.
The generated implementation is cached in the test context, to avoid duplication
of (potentially expensive) compilation.
Note: This caching introduces a dependency between tests, which is captured by an
`xdist_group` marker in combination with `--dist loadgroup` to ensure safe parallel
test execution.
Comment on lines -437 to +451
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This comment was out of date. There's no utils.ImplementationDB (anymore).

"""
backend_slug = gt_utils.slugify(test["backend"], valid_symbols="")
implementation = gtscript.stencil(
Expand All @@ -461,7 +474,8 @@ def _test_generation(cls, test, externals_dict):
or ax == "K"
or field_info.boundary[i] >= cls.global_boundaries[name][i]
)
test["implementations"].append(implementation)
assert test["implementation"] is None
test["implementation"] = implementation
Comment on lines -464 to +478
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Assert our assumption that we only ever cache one implementation per test context.


@classmethod
def _run_test_implementation(cls, parameters_dict, implementation): # too complex
Expand Down Expand Up @@ -585,16 +599,16 @@ def _run_test_implementation(cls, parameters_dict, implementation): # too compl
def _test_implementation(cls, test, parameters_dict):
"""Test computed values for implementations generated for all *backends* and *stencil suites*.

The generated implementations are reused from previous tests by means of a
:class:`utils.ImplementationsDB` instance shared at module scope.
The generated implementation was cached in the test context, to avoid duplication
of (potentially expensive) compilation.
Note: This caching introduces a dependency between tests, which is captured by an
`xdist_group` marker in combination with `--dist loadgroup` to ensure safe parallel
test execution.
"""
implementation_list = test["implementations"]
if not implementation_list:
pytest.skip(
"Cannot perform validation tests, since there are no valid implementations."
)
for implementation in implementation_list:
if not isinstance(implementation, StencilObject):
raise RuntimeError("Wrong function got from implementations_db cache!")
implementation = test["implementation"]
assert (
implementation is not None
), "Stencil implementation not found. This usually means code generation failed."
assert isinstance(implementation, StencilObject)

cls._run_test_implementation(parameters_dict, implementation)
cls._run_test_implementation(parameters_dict, implementation)