diff --git a/src/pystencils/nbackend/arrays.py b/src/pystencils/nbackend/arrays.py index 369f3d19af31fa471851fc9c9be20836034a03f6..3027c0b4096ab0a6bd8578613f2f5acd4e53d1bb 100644 --- a/src/pystencils/nbackend/arrays.py +++ b/src/pystencils/nbackend/arrays.py @@ -104,9 +104,15 @@ class PsLinearizedArray: for i, s in enumerate(strides) ) + self._base_ptr = PsArrayBasePointer(f"{self._name}_data", self) + @property def name(self): return self._name + + @property + def base_pointer(self) -> PsArrayBasePointer: + return self._base_ptr @property def shape(self) -> tuple[PsArrayShapeVar | PsTypedConstant, ...]: diff --git a/src/pystencils/nbackend/ast/kernelfunction.py b/src/pystencils/nbackend/ast/kernelfunction.py index 9aecb16eac40cbcbd84e3fbfab24fcd8ae923a39..b911f110091e251990dab4f5eb50d64733360545 100644 --- a/src/pystencils/nbackend/ast/kernelfunction.py +++ b/src/pystencils/nbackend/ast/kernelfunction.py @@ -67,7 +67,7 @@ class PsKernelFunction(PsAstNode): __match_args__ = ("body",) def __init__(self, body: PsBlock, target: Target, name: str = "kernel"): - self._body = body + self._body: PsBlock = body self._target = target self._name = name diff --git a/src/pystencils/nbackend/kernelcreation/context.py b/src/pystencils/nbackend/kernelcreation/context.py index 154e06aefa1aee5caf5027e52f1f08bd0bcf446c..28417d56407cb8764e1bc2d3023db56ced113f2e 100644 --- a/src/pystencils/nbackend/kernelcreation/context.py +++ b/src/pystencils/nbackend/kernelcreation/context.py @@ -7,18 +7,18 @@ from abc import ABC from ...field import Field from ...typing import TypedSymbol, BasicType -from ..arrays import PsLinearizedArray, PsArrayBasePointer +from ..arrays import PsLinearizedArray from ..types import PsIntegerType from ..types.quick import make_type from ..typed_expressions import PsTypedVariable, VarOrConstant from ..constraints import PsKernelConstraint +from ..exceptions import PsInternalCompilerError @dataclass -class PsFieldArrayPair: +class PsArrayDescriptor: field: Field array: PsLinearizedArray - base_ptr: PsArrayBasePointer class IterationSpace(ABC): @@ -80,47 +80,35 @@ class SparseIterationSpace(IterationSpace): class KernelCreationContext: - """Manages the translation process from the SymPy frontend to the backend AST. + """Manages the translation process from the SymPy frontend to the backend AST, and collects + all necessary information for the translation. - It does the following things: - - - Default data types: The context knows the data types that should be applied by default - to SymPy expressions. - - Management of fields. The context manages all mappings from front-end `Field`s to their - underlying `PsLinearizedArray`s. - - Collection of constraints. All constraints that arise during translation are collected in the - context, and finally attached to the kernel function object once translation is complete. Data Types ---------- - - The `index_dtype` is the data type used throughout translation for all loop counters and array indexing. - - The `default_numeric_dtype` is the data type assigned by default to all symbols occuring in SymPy assignments + The kernel creation context manages the default data types for loop limits and counters, index calculations, + and the typifier. Fields and Arrays - ----------------- - - There's several types of fields that need to be mapped to arrays. + ------------------ - - `FieldType.GENERIC` corresponds to domain fields. - Domain fields can only be accessed by relative offsets, and therefore must always - be associated with an iteration space that provides a spatial index tuple. - - `FieldType.INDEXED` are 1D arrays of index structures. They must be accessed by a single running index. - If there is at least one indexed field present there must also exist an index source for that field - (loop or device indexing). - An indexed field may itself be an index source for domain fields. - - `FieldType.BUFFER` are 1D arrays whose indices must be incremented with each access. - Within a domain, a buffer may be either written to or read from, never both. + The kernel creation context acts as a factory for mapping fields to arrays. + Iteration Space + --------------- - In the translator, frontend fields and backend arrays are managed together using the `PsFieldArrayPair` class. + The context manages the iteration space within which the current translation takes place. It may be a sparse + or full iteration space. """ def __init__(self, index_dtype: PsIntegerType): self._index_dtype = index_dtype - self._arrays: dict[Field, PsFieldArrayPair] = dict() + self._arrays: dict[Field, PsLinearizedArray] = dict() self._constraints: list[PsKernelConstraint] = [] + self._ispace: IterationSpace | None = None + @property def index_dtype(self) -> PsIntegerType: return self._index_dtype @@ -132,35 +120,47 @@ class KernelCreationContext: def constraints(self) -> tuple[PsKernelConstraint, ...]: return tuple(self._constraints) - def add_field(self, field: Field) -> PsFieldArrayPair: - arr_shape = tuple( - ( - Ellipsis if isinstance(s, TypedSymbol) else s - ) # TODO: Field should also use ellipsis - for s in field.shape - ) + def get_array(self, field: Field) -> PsLinearizedArray: + if field not in self._arrays: + arr_shape = tuple( + ( + Ellipsis if isinstance(s, TypedSymbol) else s + ) # TODO: Field should also use ellipsis + for s in field.shape + ) + + arr_strides = tuple( + ( + Ellipsis if isinstance(s, TypedSymbol) else s + ) # TODO: Field should also use ellipsis + for s in field.strides + ) - arr_strides = tuple( - ( - Ellipsis if isinstance(s, TypedSymbol) else s - ) # TODO: Field should also use ellipsis - for s in field.strides - ) + # TODO: frontend should use new type system + element_type = make_type(cast(BasicType, field.dtype).numpy_dtype.type) - # TODO: frontend should use new type system - element_type = make_type(cast(BasicType, field.dtype).numpy_dtype.type) + arr = PsLinearizedArray( + field.name, element_type, arr_shape, arr_strides, self.index_dtype + ) - arr = PsLinearizedArray( - field.name, element_type, arr_shape, arr_strides, self.index_dtype - ) + self._arrays[field] = arr - fa_pair = PsFieldArrayPair( - field=field, array=arr, base_ptr=PsArrayBasePointer("arr_data", arr) - ) + return self._arrays[field] - self._arrays[field] = fa_pair + def set_iteration_space(self, ispace: IterationSpace): + self._ispace = ispace - return fa_pair + def get_iteration_space(self) -> IterationSpace: + if self._ispace is None: + raise PsInternalCompilerError("No iteration space set in context.") + return self._ispace - def get_array_descriptor(self, field: Field): - return self._arrays[field] + def get_full_iteration_space(self) -> FullIterationSpace: + if not isinstance(self._ispace, FullIterationSpace): + raise PsInternalCompilerError("No full iteration space set in context.") + return self._ispace + + def get_sparse_iteration_space(self) -> SparseIterationSpace: + if not isinstance(self._ispace, SparseIterationSpace): + raise PsInternalCompilerError("No sparse iteration space set in context.") + return self._ispace diff --git a/src/pystencils/nbackend/kernelcreation/defaults.py b/src/pystencils/nbackend/kernelcreation/defaults.py new file mode 100644 index 0000000000000000000000000000000000000000..2ef68fe89fe989b01cdc699870b63fe06b05db9f --- /dev/null +++ b/src/pystencils/nbackend/kernelcreation/defaults.py @@ -0,0 +1,17 @@ +"""This module defines various default types, symbols and variables for use in pystencils kernels. + +On many occasions the SymPy frontend uses canonical symbols and types. +With the pymbolic-based backend, these symbols have to exist in two +variants; as `sp.Symbol` or `TypedSymbol`, and as `PsTypedVariable`s. +Therefore, for conciseness, this module should collect and provide each of these symbols. + +We might furthermore consider making the defaults collection configurable. + +A possibly incomplete list of symbols and types that need to be defined: + + - The default indexing data type (currently loosely defined as `int`) + - The default spatial iteration counters (currently defined by `LoopOverCoordinate`) + - The names of the coordinate members of index lists (currently in `CreateKernelConfig.coordinate_names`) + - The sparse iteration counter (doesn't even exist yet) + - ... +""" diff --git a/src/pystencils/nbackend/kernelcreation/domain_kernels.py b/src/pystencils/nbackend/kernelcreation/domain_kernels.py index d76ebbf2bc37958f60a18675cfdc20dac12c0eac..a66cbd12ef05e57ffc972910045aff964d7fb2a1 100644 --- a/src/pystencils/nbackend/kernelcreation/domain_kernels.py +++ b/src/pystencils/nbackend/kernelcreation/domain_kernels.py @@ -21,12 +21,6 @@ def create_domain_kernel(assignments: AssignmentCollection): check = KernelConstraintsCheck() # TODO: config check.visit(assignments) - all_fields: set[Field] = check.fields_written | check.fields_read - - # 3. Register fields - for f in all_fields: - ctx.add_field(f) - # All steps up to this point are the same in domain and indexed kernels; # the difference now comes with the iteration space. # @@ -35,7 +29,7 @@ def create_domain_kernel(assignments: AssignmentCollection): # Indexed kernels, on the other hand, have to create a sparse iteration space # from one index list. - # 4. Create iteration space + # 3. Create iteration space ghost_layers: int = NotImplemented # determine required ghost layers common_shape: tuple[ int | EllipsisType, ... @@ -45,12 +39,12 @@ def create_domain_kernel(assignments: AssignmentCollection): NotImplemented # create from ghost layers and with given shape ) - # 5. Freeze assignments + # 4. Freeze assignments # This call is the same for both domain and indexed kernels - freeze = FreezeExpressions(ctx, ispace) + freeze = FreezeExpressions(ctx) kernel_body: PsBlock = freeze(assignments) - # 6. Typify + # 5. Typify # Also the same for both types of kernels # determine_types(kernel_body) @@ -58,13 +52,13 @@ def create_domain_kernel(assignments: AssignmentCollection): # Here we might hand off the compilation to a target-specific part of the compiler # (CPU/CUDA/...), since these will likely also apply very different optimizations. - # 7. Add loops or device indexing + # 6. Add loops or device indexing # This step translates the iteration space to actual index calculation code and is once again # different in indexed and domain kernels. - # 8. Apply optimizations + # 7. Apply optimizations # - Vectorization # - OpenMP # - Loop Splitting, Tiling, Blocking - # 9. Create and return kernel function. + # 8. Create and return kernel function. diff --git a/src/pystencils/nbackend/kernelcreation/freeze.py b/src/pystencils/nbackend/kernelcreation/freeze.py index 7ae3879934366309ae8aaf5d11f5711114a13804..e9fc6ccd72bab3f9dac24f3bcd7539c9a3923d5b 100644 --- a/src/pystencils/nbackend/kernelcreation/freeze.py +++ b/src/pystencils/nbackend/kernelcreation/freeze.py @@ -3,19 +3,17 @@ from pymbolic.interop.sympy import SympyToPymbolicMapper from ...field import Field, FieldType -from .context import KernelCreationContext, IterationSpace, SparseIterationSpace +from .context import KernelCreationContext from ..ast.nodes import PsAssignment from ..types import PsSignedIntegerType, PsIeeeFloatType, PsUnsignedIntegerType from ..typed_expressions import PsTypedVariable from ..arrays import PsArrayAccess -from ..exceptions import PsInternalCompilerError class FreezeExpressions(SympyToPymbolicMapper): - def __init__(self, ctx: KernelCreationContext, ispace: IterationSpace): + def __init__(self, ctx: KernelCreationContext): self._ctx = ctx - self._ispace = ispace def map_Assignment(self, expr): # noqa lhs = self.rec(expr.lhs) @@ -44,9 +42,8 @@ class FreezeExpressions(SympyToPymbolicMapper): def map_Access(self, access: Field.Access): field = access.field - array_desc = self._ctx.get_array_descriptor(field) - array = array_desc.array - ptr = array_desc.base_ptr + array = self._ctx.get_array(field) + ptr = array.base_pointer offsets: list[pb.Expression] = [self.rec(o) for o in access.offsets] indices: list[pb.Expression] = [self.rec(o) for o in access.index] @@ -56,17 +53,14 @@ class FreezeExpressions(SympyToPymbolicMapper): case FieldType.GENERIC: # Add the iteration counters offsets = [ - i + o for i, o in zip(self._ispace.spatial_indices, offsets) + i + o for i, o in zip(self._ctx.get_iteration_space().spatial_indices, offsets) ] case FieldType.INDEXED: - if isinstance(self._ispace, SparseIterationSpace): - # TODO: make sure index (and all offsets?) are zero - # TODO: Add sparse iteration counter - raise NotImplementedError() - else: - raise PsInternalCompilerError( - "Cannot translate index field access without a sparse iteration space." - ) + # flake8: noqa + sparse_ispace = self._ctx.get_sparse_iteration_space() + # TODO: make sure index (and all offsets?) are zero + # TODO: Add sparse iteration counter + raise NotImplementedError() case FieldType.CUSTOM: raise ValueError("Custom fields support only absolute accesses.") case unknown: