diff --git a/docs/source/usage/api_modelling.md b/docs/source/usage/api_modelling.md index 24591ef25ecfc53aae6ff5675d9788d23f4bcb87..4ba552aec22a7c720d218ea72f236325db5481a0 100644 --- a/docs/source/usage/api_modelling.md +++ b/docs/source/usage/api_modelling.md @@ -229,3 +229,10 @@ b = lang.AugExpr("double").var("b") expr = MyClass(T1="int", T2="double").ctor(a, b) expr, lang.depends(expr), lang.includes(expr) ``` + +(field_data_structure_reflection)= +## Reflecting Field Data Structures + +:::{admonition} To Do + +::: diff --git a/docs/source/usage/how_to_composer.md b/docs/source/usage/how_to_composer.md index 709d72a7e06aae81569eb2714837673f94f49c13..7f08829605abad35f5603b502e2cc454df90124a 100644 --- a/docs/source/usage/how_to_composer.md +++ b/docs/source/usage/how_to_composer.md @@ -236,6 +236,7 @@ with SourceFileGenerator() as sfg: ) ``` +(how_to_namespaces)= ## Namespaces C++ uses namespaces to structure code and group entities. @@ -267,9 +268,132 @@ with SourceFileGenerator() as sfg: sfg.code("/* More code in the outer namespace */") ``` -## Kernels and Kernel Parameter Mappings +## Kernels and Parameter Mappings +The original purpose of pystencils-sfg is to simplify the embedding of *pystencils*-generated +numerical kernels into C++ applications. +This section discusses how to register kernels with the source file generator, +how to call them in wrapper code, +and how to automatically map symbolic pystencils fields onto nd-array data structures. +### Registering Kernels + +In the generated files, kernels are organized in *kernel namespaces*. +The composer gives us access to the default kernel namespace (`<current_namespace>::kernels`) +via `sfg.kernels`. + +To add a kernel, + - either pass its assignments and the pystencils code generator configuration directly to {any}`kernels.reate() <KernelsAdder.create>`, + - or create the kernel separately through {any}`pystencils.create_kernel <pystencils.codegen.create_kernel>` and register it using + {any}`kernels.add() <KernelsAdder.add>`. + +Both functions return a kernel handle, through which the kernel may later be invoked. + +You may create and access custom-named kernel namespaces using {any}`sfg.kernel_namespace() <SfgBasicComposer.kernel_namespace>`. +This gives you a {any}`KernelsAdder` object with the same interface as `sfg.kernels`. + +:::{note} + +A kernel namespace is not a regular namespace; if you attempt to create both a regular and a kernel namespace with the same name, +the composer will raise an error. +::: + +Here's an example with two kernels being registered in different kernel namespace, +once using `add`, and once using `create`. + +```{code-cell} ipython3 +import pystencils as ps + +with SourceFileGenerator() as sfg: + # Create symbolic fields + f, g = ps.fields("f, g: double[2D]") + + # Define and create the first kernel + asm1 = ps.Assignment(f(0), g(0)) + cfg1 = ps.CreateKernelConfig() + cfg1.cpu.openmp.enable = True + khandle_1 = sfg.kernels.create(asm1, "first_kernel", cfg1) + + # Define the second kernel and its codegen configuration + asm2 = ps.Assignment(f(0), 3.0 * g(0)) + cfg2 = ps.CreateKernelConfig(target=ps.Target.CUDA) + + # Create and register the second kernel at a custom namespace + kernel2 = ps.create_kernel(asm2, cfg2) + khandle_2 = sfg.kernel_namespace("gpu_kernels").add(kernel2, "second_kernel") +``` + +### Writing Kernel Wrapper Functions + +By default, kernel definitions are only visible in the generated implementation file; +kernels are supposed to not be called directly, but through wrapper functions. +This serves to hide their fairly lenghty and complicated low-level function interface. + +#### Invoking CPU Kernels + +To call a CPU kernel from a function, use `sfg.call` on a kernel handle: + +```{code-block} python +sfg.function("kernel_wrapper")( + sfg.call(khandle) +) +``` + +This will expose all parameters of the kernel into the wrapper function and, in turn, +cause them to be added to its signature. +We don't want to expose this complexity, but instead hide it by using appropriate data structures. +The next section explains how that is achieved in pystencils-sfg. + +#### Mapping Fields to Data Structures + +Pystencils kernels operate on n-dimensional contiguous or strided arrays, +There exist many classes with diverse APIs modelling such arrays throughout the scientific +computing landscape, including [Kokkos Views][kokkos_view], [C++ std::mdspan][mdspan], +[SYCL buffers][sycl_buffer], and many framework-specific custom-built classes. +Using the protocols behind {any}`sfg.map_field <SfgBasicComposer.map_field>`, +it is possible to automatically emit code +that extracts the indexing information required by a kernel from any of these classes +- provided a suitable API reflection is available. + +:::{seealso} +[](#field_data_structure_reflection) for instructions on how to set up field API +reflection for a custom nd-array data structure. +::: + +Pystencils-sfg natively provides field extraction for a number of C++ STL-classes, +such as `std::vector` and `std::span` (for 1D fields) and `std::mdspan`. +Import any of them from `pystencilssfg.lang.cpp.std` and create an instance for a given +field using `.from_field()`. +Then, inside the wrapper function, pass the symbolic field and its associated data structure to +{any}`sfg.map_field <SfgBasicComposer.map_field>`. +before calling the kernel: + +```{code-cell} ipython3 +import pystencils as ps +from pystencilssfg.lang.cpp import std + +with SourceFileGenerator() as sfg: + # Create symbolic fields + f, g = ps.fields("f, g: double[1D]") + + # Create data structure reflections + f_vec = std.vector.from_field(f) + g_span = std.span.from_field(g) + + # Create the kernel + asm = ps.Assignment(f(0), g(0)) + khandle = sfg.kernels.create(asm, "my_kernel") + + # Create the wrapper function + sfg.function("call_my_kernel")( + sfg.map_field(f, f_vec), + sfg.map_field(g, g_span), + sfg.call(khandle) + ) +``` + +(exposed_inline_kernels)= +### Exposed and Inline Kernels :::{admonition} To Do @@ -279,3 +403,7 @@ with SourceFileGenerator() as sfg: ::: + +[kokkos_view]: https://kokkos.org/kokkos-core-wiki/ProgrammingGuide/View.html +[mdspan]: https://en.cppreference.com/w/cpp/container/mdspan +[sycl_buffer]: https://registry.khronos.org/SYCL/specs/sycl-2020/html/sycl-2020.html#subsec:buffers diff --git a/src/pystencilssfg/composer/basic_composer.py b/src/pystencilssfg/composer/basic_composer.py index 7c61b61e2940b3c05227005380de8f7d151f4763..6a7802f26d86a4ae251344aca6e304de45339ca9 100644 --- a/src/pystencilssfg/composer/basic_composer.py +++ b/src/pystencilssfg/composer/basic_composer.py @@ -5,7 +5,13 @@ import sympy as sp from functools import reduce from warnings import warn -from pystencils import Field, CreateKernelConfig, create_kernel +from pystencils import ( + Field, + CreateKernelConfig, + create_kernel, + Assignment, + AssignmentCollection, +) from pystencils.codegen import Kernel from pystencils.types import create_type, UserTypeSpec, PsType @@ -79,6 +85,8 @@ SequencerArg: TypeAlias = tuple | ExprLike | SfgCallTreeNode | SfgNodeBuilder class KernelsAdder: + """Handle on a kernel namespace that permits registering kernels.""" + def __init__(self, ctx: SfgContext, loc: SfgNamespaceBlock): self._ctx = ctx self._loc = loc @@ -115,7 +123,7 @@ class KernelsAdder: def create( self, - assignments, + assignments: Assignment | Sequence[Assignment] | AssignmentCollection, name: str | None = None, config: CreateKernelConfig | None = None, ): @@ -135,7 +143,6 @@ class KernelsAdder: config.function_name = name - # type: ignore kernel = create_kernel(assignments, config=config) return self.add(kernel) @@ -263,10 +270,10 @@ class SfgBasicComposer(SfgIComposer): return self.kernel_namespace("kernels") def kernel_namespace(self, name: str) -> KernelsAdder: - """Return the kernel namespace of the given name, creating it if it does not exist yet.""" - kns = self._cursor.get_entity("kernels") + """Return a view on a kernel namespace in order to add kernels to it.""" + kns = self._cursor.get_entity(name) if kns is None: - kns = SfgKernelNamespace("kernels", self._cursor.current_namespace) + kns = SfgKernelNamespace(name, self._cursor.current_namespace) self._cursor.add_entity(kns) elif not isinstance(kns, SfgKernelNamespace): raise ValueError( @@ -511,7 +518,7 @@ class SfgBasicComposer(SfgIComposer): Args: field: The pystencils field to be mapped - src_object: An object representing a field data structure. + index_provider: An expression representing a field, or a field extraction provider instance cast_indexing_symbols: Whether to always introduce explicit casts for indexing symbols """ return SfgDeferredFieldMapping(