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

refactor[cartesian]: use DeviceType from core_defn in LayoutInfo #1867

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
39 changes: 19 additions & 20 deletions src/gt4py/cartesian/backend/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@
REGISTRY = gt_utils.Registry()


def from_name(name: str) -> Optional[Type[Backend]]:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unrelated: The return type is not optional. It raises an exception in case the backend is not found.

def from_name(name: str) -> Type[Backend]:
backend = REGISTRY.get(name, None)
if not backend:
if backend is None:
raise NotImplementedError(
f"Backend {name} is not implemented, options are: {REGISTRY.names}"
)
Expand Down Expand Up @@ -84,7 +84,7 @@ class Backend(abc.ABC):
#: Backend-specific storage parametrization:
#:
#: - "alignment": in bytes
#: - "device": "cpu" | "gpu"
#: - "device": core_defs.DeviceType | None
Copy link
Contributor

Choose a reason for hiding this comment

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

Just a question: why is None allowed here? It shouldn't be allowed if the old documentation is correct, right?

#: - "layout_map": callback converting a mask to a layout
#: - "is_optimal_layout": callback checking if a storage has compatible layout
storage_info: ClassVar[gt_storage.layout.LayoutInfo]
Expand Down Expand Up @@ -435,21 +435,20 @@ def disabled(message: str, *, enabled_env_var: str) -> Callable[[Type[Backend]],
enabled = bool(int(os.environ.get(enabled_env_var, "0")))
if enabled:
return deprecated(message)
else:

def _decorator(cls: Type[Backend]) -> Type[Backend]:
def _no_generate(obj) -> Type[StencilObject]:
raise NotImplementedError(
f"Disabled '{cls.name}' backend: 'f{message}'\n",
f"You can still enable the backend by hand using the environment variable '{enabled_env_var}=1'",
)

# Replace generate method with raise
if not hasattr(cls, "generate"):
raise ValueError(f"Coding error. Expected a generate method on {cls}")
# Flag that it got disabled for register lookup
cls.disabled = True # type: ignore
cls.generate = _no_generate # type: ignore
return cls

return _decorator
def _decorator(cls: Type[Backend]) -> Type[Backend]:
def _no_generate(obj) -> Type[StencilObject]:
raise NotImplementedError(
f"Disabled '{cls.name}' backend: 'f{message}'\n",
f"You can still enable the backend by hand using the environment variable '{enabled_env_var}=1'",
)

# Replace generate method with raise
if not hasattr(cls, "generate"):
raise ValueError(f"Coding error. Expected a generate method on {cls}")
# Flag that it got disabled for register lookup
cls.disabled = True # type: ignore
cls.generate = _no_generate # type: ignore
return cls

return _decorator
Comment on lines +437 to +452
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unrelated: no else needed after return inside if. Reduces the amount of indentation needed.

24 changes: 4 additions & 20 deletions src/gt4py/cartesian/backend/dace_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@ def __call__(self, stencil_ir: gtir.Stencil) -> Dict[str, Dict[str, str]]:
stencil_ir, sdfg, module_name=self.module_name, backend=self.backend
)

bindings_ext = "cu" if self.backend.storage_info["device"] == "gpu" else "cpp"
bindings_ext = "cu" if self.backend.name.endswith(":gpu") else "cpp"
sources = {
"computation": {"computation.hpp": implementation},
"bindings": {f"bindings.{bindings_ext}": bindings},
Expand Down Expand Up @@ -674,9 +674,7 @@ def generate_entry_params(self, stencil_ir: gtir.Stencil, sdfg: dace.SDFG) -> Li
assert isinstance(data, dace.data.Array)
res[name] = (
"py::{pybind_type} {name}, std::array<gt::int_t,{ndim}> {name}_origin".format(
pybind_type=(
"object" if self.backend.storage_info["device"] == "gpu" else "buffer"
),
pybind_type=("object" if self.backend.name.endswith(":gpu") else "buffer"),
name=name,
ndim=len(data.shape),
)
Expand Down Expand Up @@ -777,14 +775,7 @@ def generate(self) -> Type[StencilObject]:
class DaceCPUBackend(BaseDaceBackend):
name = "dace:cpu"
languages = {"computation": "c++", "bindings": ["python"]}
storage_info = {
"alignment": 1,
"device": "cpu",
"layout_map": gt_storage.layout.layout_maker_factory((0, 1, 2)),
"is_optimal_layout": gt_storage.layout.layout_checker_factory(
gt_storage.layout.layout_maker_factory((0, 1, 2))
),
}
Comment on lines -780 to -787
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved to gt_storage.layout like all the other layouts

storage_info = gt_storage.layout.DaceCPULayout
MODULE_GENERATOR_CLASS = DaCePyExtModuleGenerator

options = BaseGTBackend.GT_BACKEND_OPTS
Expand All @@ -799,14 +790,7 @@ class DaceGPUBackend(BaseDaceBackend):

name = "dace:gpu"
languages = {"computation": "cuda", "bindings": ["python"]}
storage_info = {
"alignment": 32,
"device": "gpu",
"layout_map": gt_storage.layout.layout_maker_factory((2, 1, 0)),
"is_optimal_layout": gt_storage.layout.layout_checker_factory(
gt_storage.layout.layout_maker_factory((2, 1, 0))
),
}
storage_info = gt_storage.layout.DaceGPULayout
MODULE_GENERATOR_CLASS = DaCeCUDAPyExtModuleGenerator
options = {**BaseGTBackend.GT_BACKEND_OPTS, "device_sync": {"versioning": True, "type": bool}}

Expand Down
7 changes: 2 additions & 5 deletions src/gt4py/cartesian/backend/dace_stencil_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,11 @@
from gt4py.cartesian.utils import shash


def _extract_array_infos(field_args, device) -> Dict[str, Optional[ArgsInfo]]:
def _extract_array_infos(field_args) -> Dict[str, Optional[ArgsInfo]]:
return {
name: ArgsInfo(
array=arg,
dimensions=getattr(arg, "__gt_dims__", None),
device=device,
origin=getattr(arg, "__gt_origin__", None),
)
for name, arg in field_args.items()
Expand Down Expand Up @@ -186,9 +185,7 @@ def normalize_args(
args_as_kwargs = {
name: (kwargs[name] if name in kwargs else next(args_iter)) for name in arg_names
}
arg_infos = _extract_array_infos(
field_args=args_as_kwargs, device=backend_cls.storage_info["device"]
)
arg_infos = _extract_array_infos(field_args=args_as_kwargs)

origin = DaCeStencilObject._normalize_origins(arg_infos, field_info, origin)

Expand Down
3 changes: 2 additions & 1 deletion src/gt4py/cartesian/backend/gtc_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from gt4py.cartesian.gtc.passes.gtir_pipeline import GtirPipeline
from gt4py.cartesian.gtc.passes.oir_pipeline import OirPipeline
from gt4py.eve.codegen import MakoTemplate as as_mako
from gt4py.storage.cartesian.layout import is_gpu_device


if TYPE_CHECKING:
Expand Down Expand Up @@ -51,7 +52,7 @@ def pybuffer_to_sid(
domain_ndim = domain_dim_flags.count(True)
sid_ndim = domain_ndim + data_ndim

as_sid = "as_cuda_sid" if backend.storage_info["device"] == "gpu" else "as_sid"
as_sid = "as_cuda_sid" if is_gpu_device(backend.storage_info) else "as_sid"

sid_def = """gt::{as_sid}<{ctype}, {sid_ndim},
gt::integral_constant<int, {unique_index}>>({name})""".format(
Expand Down
3 changes: 2 additions & 1 deletion src/gt4py/cartesian/backend/gtcpp_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from gt4py.cartesian.gtc.passes.gtir_pipeline import GtirPipeline
from gt4py.cartesian.gtc.passes.oir_pipeline import DefaultPipeline
from gt4py.eve import codegen
from gt4py.storage.cartesian.layout import is_gpu_device

from .gtc_common import BaseGTBackend, CUDAPyExtModuleGenerator

Expand Down Expand Up @@ -85,7 +86,7 @@ def visit_FieldDecl(self, node: gtcpp.FieldDecl, **kwargs):
if kwargs["external_arg"]:
return "py::{pybind_type} {name}, std::array<gt::int_t,{sid_ndim}> {name}_origin".format(
pybind_type=(
"object" if self.backend.storage_info["device"] == "gpu" else "buffer"
"object" if is_gpu_device(self.backend.storage_info) else "buffer"
),
name=node.name,
sid_ndim=sid_ndim,
Expand Down
12 changes: 5 additions & 7 deletions src/gt4py/cartesian/stencil_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from dataclasses import dataclass
from numbers import Number
from pickle import dumps
from typing import Any, Callable, ClassVar, Dict, Literal, Optional, Tuple, Union, cast
from typing import Any, Callable, ClassVar, Dict, Optional, Tuple, Union, cast

import numpy as np

Expand All @@ -24,6 +24,7 @@
from gt4py import cartesian as gt4pyc
from gt4py.cartesian.definitions import AccessKind, DomainInfo, FieldInfo, ParameterInfo
from gt4py.cartesian.gtc.definitions import Index, Shape
from gt4py.storage.cartesian.layout import LayoutInfo


try:
Expand Down Expand Up @@ -51,22 +52,21 @@ def _compute_domain_origin_cache_key(

@dataclass
class ArgsInfo:
device: str
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Never used, so I removed it

array: FieldType
original_object: Optional[Any] = None
origin: Optional[Tuple[int, ...]] = None
dimensions: Optional[Tuple[str, ...]] = None


def _extract_array_infos(
field_args: Dict[str, Optional[FieldType]], device: Literal["cpu", "gpu"]
field_args: Dict[str, Optional[FieldType]], layout_info: LayoutInfo
) -> Dict[str, Optional[ArgsInfo]]:
array_infos: Dict[str, Optional[ArgsInfo]] = {}
for name, arg in field_args.items():
if arg is None:
array_infos[name] = None
else:
array = storage_utils.asarray(arg, device=device)
array = storage_utils.asarray(arg, layout_info=layout_info)
dimensions = storage_utils.get_dims(arg)
if dimensions is not None:
sorted_dimensions = [d for d in "IJK" if d in dimensions]
Expand All @@ -79,7 +79,6 @@ def _extract_array_infos(
array=array,
original_object=arg,
dimensions=dimensions,
device=device,
origin=storage_utils.get_origin(arg),
)
return array_infos
Expand Down Expand Up @@ -562,8 +561,7 @@ def _call_run(
exec_info["call_run_start_time"] = time.perf_counter()
backend_cls = gt4pyc.backend.from_name(self.backend)
assert backend_cls is not None
device = backend_cls.storage_info["device"]
array_infos = _extract_array_infos(field_args, device)
array_infos = _extract_array_infos(field_args, backend_cls.storage_info)

cache_key = _compute_domain_origin_cache_key(array_infos, parameter_args, domain, origin)
if cache_key not in self._domain_origin_cache:
Expand Down
24 changes: 24 additions & 0 deletions src/gt4py/cartesian/testing/definitions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# GT4Py - GridTools Framework
#
# Copyright (c) 2014-2024, ETH Zurich
# All rights reserved.
#
# Please, refer to the LICENSE file in the root directory.
# SPDX-License-Identifier: BSD-3-Clause

from gt4py.cartesian.backend.base import REGISTRY as BACKEND_REGISTRY


# TODO (romanc)
# This file can move to the tests/ folder once we refactor the `StencilTestSuite`
# class (i.e. https://github.com/GEOS-ESM/NDSL/issues/72). The stencil test suites
# use the `GPU_BACKEND_NAMES` and I didn't wanna have `gt4py/cartesian/testing` depend
# on the `tests/` directory. That sounded the wrong way around, so I moved them here
# for now.

ALL_BACKEND_NAMES = list(BACKEND_REGISTRY.keys())

GPU_BACKEND_NAMES = ["cuda", "gt:gpu", "dace:gpu"]
CPU_BACKEND_NAMES = [name for name in ALL_BACKEND_NAMES if name not in GPU_BACKEND_NAMES]

PERFORMANCE_BACKEND_NAMES = [name for name in ALL_BACKEND_NAMES if name not in ("numpy", "cuda")]
5 changes: 3 additions & 2 deletions src/gt4py/cartesian/testing/suites.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from gt4py.cartesian.definitions import AccessKind, FieldInfo
from gt4py.cartesian.gtc.definitions import Boundary, CartesianSpace, Index, Shape
from gt4py.cartesian.stencil_object import StencilObject
from gt4py.cartesian.testing.definitions import GPU_BACKEND_NAMES
from gt4py.storage.cartesian import utils as storage_utils

from .input_strategies import (
Expand Down Expand Up @@ -206,7 +207,7 @@ def hyp_wrapper(test_hyp, hypothesis_data):
)

marks = test["marks"].copy()
if gt4pyc.backend.from_name(test["backend"]).storage_info["device"] == "gpu":
if test["backend"] in GPU_BACKEND_NAMES:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We come by here during test setup. This happens for CPU/GPU tests alike. In case cupy isn't automatically detected (or in case there's no GPU (e.g. on GHA runners), CUPY_DEVICE will not be defined and default to None and this won't correctly trigger the requires_gpu marks.

For the test system, we thus need something that is independent of cupy availability to denote that a certain tests require GPU while other don't. This could also be done differently, e.g. a requires_gpu property on the base Backend class. Honestly, the longer I think about it, the more it feels to me that we should be able to tell from the backend. If there were multiple layouts per backend (as far as I understand this is possible), I'd expect all of them to either run on CPU or GPU (or possibly another accelerator in the future).

Copy link
Contributor

Choose a reason for hiding this comment

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

Agreed. I think the earlier design was to split layout from backend by addressing them by their underlying allocator (numpy/cupy) to the backend. Though it's correct architecturally, I would argue it's a bit of an over engineering and repeating data is not an issue. Which leads me to the fact that Backend should be the source of truth, including for requires_gpu

Copy link
Contributor

Choose a reason for hiding this comment

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

And we could extend it even further by re-routing allocation through, etc. I am no fan of massive class that does it all, but in this case Backend seems underused a single source of truth

Copy link
Contributor

Choose a reason for hiding this comment

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

Probably out of scope for this PR

marks.append(pytest.mark.requires_gpu)
# Run generation and implementation tests in the same group to ensure
# (thread-) safe parallelization of stencil tests.
Expand Down Expand Up @@ -240,7 +241,7 @@ def hyp_wrapper(test_hyp, hypothesis_data):
)

marks = test["marks"].copy()
if gt4pyc.backend.from_name(test["backend"]).storage_info["device"] == "gpu":
if test["backend"] in GPU_BACKEND_NAMES:
marks.append(pytest.mark.requires_gpu)
# Run generation and implementation tests in the same group to ensure
# (thread-) safe parallelization of stencil tests.
Expand Down
24 changes: 24 additions & 0 deletions src/gt4py/storage/cartesian/cupy_device.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# GT4Py - GridTools Framework
#
# Copyright (c) 2014-2024, ETH Zurich
# All rights reserved.
#
# Please, refer to the LICENSE file in the root directory.
# SPDX-License-Identifier: BSD-3-Clause

from __future__ import annotations

from typing import Final, Literal

from gt4py._core.definitions import DeviceType


try:
import cupy as cp
except ImportError:
cp = None


CUPY_DEVICE: Final[Literal[DeviceType.CUDA, DeviceType.ROCM] | None] = (
None if not cp else (DeviceType.ROCM if cp.cuda.get_hipcc_path() else DeviceType.CUDA)
)
Comment on lines +22 to +24
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could share this between next and cartesian. If so, should it go into _core?

11 changes: 8 additions & 3 deletions src/gt4py/storage/cartesian/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,15 @@ def empty(
_error_on_invalid_preset(backend)
storage_info = layout.from_name(backend)
assert storage_info is not None
if storage_info["device"] == "gpu":

if storage_info["device"] is None:
raise ValueError("device is None")
elif layout.is_gpu_device(storage_info):
allocate_f = storage_utils.allocate_gpu
else:
elif layout.is_cpu_device(storage_info):
allocate_f = storage_utils.allocate_cpu
else:
raise ValueError("Unknown device")

aligned_index, shape, dtype, dimensions = storage_utils.normalize_storage_spec(
aligned_index, shape, dtype, dimensions
Expand Down Expand Up @@ -322,6 +327,6 @@ def from_array(

layout_info = layout.from_name(backend)
assert layout_info is not None
storage[...] = storage_utils.asarray(data, device=layout_info["device"])
storage[...] = storage_utils.asarray(data, layout_info=layout_info)

return storage
Loading