Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
fa798a2
Not fully working, but a huge step forward.
philip-paul-mueller Jul 22, 2026
ed76225
Let's try this.
philip-paul-mueller Jul 22, 2026
75c1945
Next small addition.
philip-paul-mueller Jul 22, 2026
88a3929
This works for one sdfg, but it is probably not fast and it has the p…
philip-paul-mueller Jul 22, 2026
0c151fa
This works it is not nice but it works.
philip-paul-mueller Jul 24, 2026
0e58c03
Now there are errors.
philip-paul-mueller Jul 27, 2026
c46e4e5
Merge remote-tracking branch 'gt4py/main' into dace_nanobind_integration
philip-paul-mueller Jul 27, 2026
f407074
Let's test that thing.
philip-paul-mueller Jul 27, 2026
63ecac9
Updated the test.
philip-paul-mueller Jul 28, 2026
516088b
First version that uses the new calling convention, but it ignores th…
philip-paul-mueller Jul 28, 2026
9e7b5ea
This works, but is not fully nice.
philip-paul-mueller Jul 28, 2026
4ad8c88
Updated DaCe.
philip-paul-mueller Jul 28, 2026
a96749f
A bit nicer.
philip-paul-mueller Jul 28, 2026
96a09bc
Updated.
philip-paul-mueller Jul 28, 2026
1558b42
Merge remote-tracking branch 'gt4py/main' into dace_nanobind_integration
philip-paul-mueller Jul 29, 2026
822fabd
Newest DaCe needs C++20.
philip-paul-mueller Jul 29, 2026
a3e1e7c
Newest DaCe needs C++20.
philip-paul-mueller Jul 29, 2026
94a0033
Removed an redundant import.
philip-paul-mueller Jul 29, 2026
113adb4
Revert "Newest DaCe needs C++20."
philip-paul-mueller Jul 29, 2026
bcc3992
Merge remote-tracking branch 'fork/dace_cpp20' into dace_nanobind_int…
philip-paul-mueller Jul 29, 2026
f171934
Small error
philip-paul-mueller Jul 29, 2026
2ca2ae9
Forgot to undo the hack.
philip-paul-mueller Jul 29, 2026
2a48a7e
A further divergence.
philip-paul-mueller Jul 29, 2026
cd79a91
Updated DaCe.
philip-paul-mueller Jul 29, 2026
002a151
Fixed an issue in a test, but we have to rework that anyway.
philip-paul-mueller Jul 29, 2026
f4ae459
Added new function to get the origin in a DaCe compatiple format.
philip-paul-mueller Jul 29, 2026
73e302f
Not yet tested, but I think we can now use it, in some way.
philip-paul-mueller Jul 29, 2026
7bb8760
I am not proud of it but seems to work.
philip-paul-mueller Jul 29, 2026
9dbe0c9
Let's see what CI sais to the newest version.
philip-paul-mueller Jul 29, 2026
dd15370
Forcing types on a typeless language is like like (whatever you think…
philip-paul-mueller Jul 31, 2026
84d60da
Relocated the code for generating `user_args`. Not ideal but currentl…
philip-paul-mueller Jul 31, 2026
d7fd2b6
Fixed a small bug.
philip-paul-mueller Jul 31, 2026
5a6c241
Merge remote-tracking branch 'gt4py/main' into dace_nanobind_integration
philip-paul-mueller Jul 31, 2026
8e8e91f
Updated dace.
philip-paul-mueller Jul 31, 2026
c2136e2
This makes the bindings code a bit nicer.
philip-paul-mueller Jul 31, 2026
8b2b2bb
Updated the decorator.
philip-paul-mueller Jul 31, 2026
8673709
Small Fix.
philip-paul-mueller Jul 31, 2026
703c577
Small cleanup.
philip-paul-mueller Jul 31, 2026
82163b6
Fixed a small bug.
philip-paul-mueller Jul 31, 2026
dde7a4e
Fixed a bug.
philip-paul-mueller Jul 31, 2026
e109d84
Had to place the function better.
philip-paul-mueller Jul 31, 2026
ffa095f
Deleted the old files that are no longer needed.
philip-paul-mueller Jul 31, 2026
be33c8a
Fixed some unit tests.
philip-paul-mueller Jul 31, 2026
1268843
Had to put them back to make the orecstrator happy.
philip-paul-mueller Jul 31, 2026
304f931
Fixed the test.
philip-paul-mueller Jul 31, 2026
a516466
Updated the unit tests.
philip-paul-mueller Jul 31, 2026
1fbd203
Fixed some tests.
philip-paul-mueller Aug 1, 2026
fdd509b
Addressed some review comments.
philip-paul-mueller Aug 4, 2026
3c1b44f
Some more changes and now it is also filtered out.
philip-paul-mueller Aug 4, 2026
35aa61d
Addressed some reviews comments, but some unit tests need updating an…
philip-paul-mueller Aug 5, 2026
6cf1c1b
Relocated the bindings code to the bindings module, i.e. the call.
philip-paul-mueller Aug 5, 2026
334084b
Small modifications.
philip-paul-mueller Aug 5, 2026
64fedd6
Updated the tests.
philip-paul-mueller Aug 5, 2026
3256fcd
Merge remote-tracking branch 'gt4py/main' into dace_nanobind_integration
philip-paul-mueller Aug 5, 2026
0d6a831
Updated DaCe.
philip-paul-mueller Aug 5, 2026
b424d10
Update.
philip-paul-mueller Aug 7, 2026
45afe7d
Now use the proper eve code thing.
philip-paul-mueller Aug 7, 2026
59eac19
Merge remote-tracking branch 'gt4py/main' into dace_nanobind_integration
philip-paul-mueller Aug 7, 2026
9577ea3
Updated DaCe.
philip-paul-mueller Aug 7, 2026
c2d9337
Let's hope that this helps.
philip-paul-mueller Aug 7, 2026
6318226
Merge remote-tracking branch 'gt4py/main' into dace_nanobind_integration
philip-paul-mueller Aug 8, 2026
e68f8e9
Fixed the unit tests.
philip-paul-mueller Aug 9, 2026
f6d341c
The file has become empty, so removing it.
philip-paul-mueller Aug 9, 2026
26476f5
Merge remote-tracking branch 'gt4py/main' into dace_nanobind_integration
philip-paul-mueller Aug 11, 2026
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
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,9 @@ url = 'https://gridtools.github.io/pypi/'
# Add the uv source below to pull dace from the gridtools index instead of PyPI:
[tool.uv.sources]
atlas4py = {index = "test.pypi"}
dace = [
{git = "https://github.com/philip-paul-mueller/dace", branch = "nanobind-compiled-sdfg"}
]

# -- versioningit --
[tool.versioningit]
Expand Down
5 changes: 5 additions & 0 deletions src/gt4py/next/embedded/nd_array_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,11 @@ def __gt_origin__(self) -> tuple[int, ...]:
assert common.Domain.is_finite(self.domain)
return tuple(-r.start for r in self.domain.ranges)

@functools.cached_property
def __dace_origin__(self) -> tuple[int, ...]:
assert common.Domain.is_finite(self.domain)
return tuple(r.start for r in self.domain.ranges)

@functools.cached_property
def __gt_buffer_info__(self) -> common.BufferInfo:
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1018,7 +1018,13 @@ def _add_sdfg_params(
transient=True,
)

# the list of all sdfg arguments (aka non-transient arrays) which include tuple-element fields
# The list of the SDFG arguments, i.e. the global arrays, scalars and free symbols.
# Note that tuple arguments are flattened and their name is mangled and no longer
# matches the name in the signature of the field operator / program.
# Also note that some scalar arguments (which are lowered to symbols) listed in
# this signature might not be part of the generated C-API, as unused symbols are
# pruned from the SDFG during optimization.
# NOTE: The dispatch code does not use it, instead the `user_args` are used.
Comment thread
philip-paul-mueller marked this conversation as resolved.
return [arg_name for arg_name, _ in sdfg_args]

def visit_Program(self, node: gtir.Program) -> dace.SDFG:
Expand Down Expand Up @@ -1064,10 +1070,10 @@ def visit_Program(self, node: gtir.Program) -> dace.SDFG:
assert isinstance(nsdfg.arrays[data], dace.data.Array)
nsdfg.arrays.pop(data)

# Create the call signature for the SDFG.
# Only the arguments required by the GT4Py program, i.e. `node.params`, are added
# as positional arguments. The implicit arguments, such as the offset providers or
# the arguments created by the translation process, must be passed as keyword arguments.
# NOTE: A program uses the `user_args` mechanism to perform the call. So,
# technically these arguments is not needed. However, the orchestrator needs

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
# technically these arguments is not needed. However, the orchestrator needs
# technically `sdfg.arg_names` is not needed. However, the orchestrator needs

# it to work. Note that in the following list, tuple arguments to the
# program/fieldop are expanded.
sdfg.arg_names = sdfg_arg_names

return sdfg
Expand Down
455 changes: 205 additions & 250 deletions src/gt4py/next/program_processors/runners/dace/workflow/bindings.py

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,13 @@ def set_dace_config(
# `gt4py.next.program_processors.runners.dace.transfromations.gpu_utils.gt_gpu_transform_non_standard_memlet()`.
dace.Config.set("compiler.cuda.allow_implicit_memlet_to_map", value=False)

# FORCE NANOBIND
dace.Config.set("compiler.interface", value="nanobind")
dace.Config.set(
"compiler.nanobind_name_collision", value="error"
) # Consider switching to `rename`.
dace.Config.set("compiler.nanobind_reuse_loaded", value=True)

if cmake_build_type is not None:
dace.Config.set("compiler.build_type", value=cmake_build_type.value)

Expand Down
160 changes: 62 additions & 98 deletions src/gt4py/next/program_processors/runners/dace/workflow/compilation.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,10 @@
from __future__ import annotations

import dataclasses
import json
import os
import pathlib
import warnings
from collections.abc import Callable, MutableSequence, Sequence
from typing import Any, Final, TypeAlias
from collections.abc import Callable
from typing import Any, Final, Sequence, TypeAlias

import dace
import dace.codegen.compiler as dace_compiler
Expand Down Expand Up @@ -126,61 +124,71 @@ def _validate_external_workspace(
class CompiledDaceProgram:
sdfg_program: dace.CompiledSDFG

# Sorted list of SDFG arguments as they appear in program ABI and corresponding data type;
# scalar arguments that are not used in the SDFG will not be present.
sdfg_argtypes: list[dace.dtypes.Data]

# The compiled program contains a callable object to update the SDFG arguments list.
update_sdfg_ctype_arglist: Callable[
[
core_defs.DeviceType,
Sequence[dace.dtypes.Data],
Sequence[Any],
MutableSequence[Any],
common.OffsetProvider,
],
None,
# Callable to process the GT4Py arguments and offset providers to bring them in a form suitable for calling.
argument_preprocessing_function: Callable[
[Sequence[Any], common.OffsetProvider, int, Any], tuple[Any, ...]
]

# Processed argument vectors that are passed to `CompiledSDFG.fast_call()`. `None`
# means that it has not been initialized, i.e. no call was ever performed.
# - csdfg_argv: Arguments used for calling the actual compiled SDFG, will be updated.
# - csdfg_init_argv: Arguments used for initialization; used only the first time and
# never updated.
csdfg_argv: MutableSequence[Any] | None
csdfg_init_argv: Sequence[Any] | None
external_workspace: gtx_wfdcommon.ExternalWorkspace | None = (
None # This attribute is set at runtime, before the first call.
)

# Whether the SDFG has transients with `AllocationLifetime.External`, i.e. whether
# the caller has to install a workspace before the program can run. Determined
# once at load time so that the (non-trivial) workspace configuration is only
# performed for the programs that actually need it.
requires_external_workspace: bool

def __init__(
self,
program: dace.CompiledSDFG,
bind_func_name: str,
binding_source_code: str,
):
self.sdfg_program = program

# `dace.CompiledSDFG.arglist()` returns an ordered dictionary that maps the argument
# name to its data type, in the same order as arguments appear in the program ABI.
# This is also the same order of arguments in `dace.CompiledSDFG._lastargs[0]`.
self.sdfg_argtypes = list(program.sdfg.arglist().values())
self.requires_external_workspace = any(
desc.lifetime == dace.dtypes.AllocationLifetime.External
for _, _, desc in program.sdfg.arrays_recursive()
)

# The binding source code is Python tailored to this specific SDFG.
# We dynamically compile that function and add it to the compiled program.
global_namespace: dict[str, Any] = {}
exec(binding_source_code, global_namespace)
self.update_sdfg_ctype_arglist = global_namespace[bind_func_name]
# For debug purpose, we set a unique module name on the compiled function.
self.update_sdfg_ctype_arglist.__module__ = os.path.basename(program.sdfg.build_folder)

# Since the SDFG hasn't been called yet.
self.csdfg_argv = None
self.csdfg_init_argv = None
self.argument_preprocessing_function = global_namespace[bind_func_name]
# For debug purpose, we set a unique module name on the compiled function.
self.argument_preprocessing_function.__module__ = os.path.basename(
program.sdfg.build_folder
)

def _configure_external_workspace(self, **kwargs: Any) -> None:
def configure_external_workspace(self, **kwargs: Any) -> None:
"""Install the caller-provided workspace buffers on the compiled SDFG.

This eagerly initializes the SDFG state, queries the required workspace
size per storage type and hands the matching buffer of
`self.external_workspace` to DaCe. It has to run before the first call,
because an SDFG with external transients refuses to run with no
workspace installed.

The required sizes depend on the symbol values passed here, so they are
only valid for calls made with the same symbols. This is called once,
with the arguments of the first call; keeping the workspace large enough
for subsequent calls is the caller's responsibility.

Args:
kwargs: The SDFG call arguments. Only the free symbols among them are
used; the remaining entries are ignored by DaCe.

Raises:
RuntimeError: If the SDFG needs a workspace but none was set for the
required device.
TypeError: If a workspace buffer exposes no suitable array interface.
ValueError: If a workspace buffer is too small, or is required for an
unsupported storage type.
"""
self.sdfg_program.initialize(**kwargs)
if workspace_sizes := self.sdfg_program.get_workspace_sizes():
if workspace_sizes := self.sdfg_program.get_workspace_sizes(**kwargs):
if self.external_workspace is None:
raise RuntimeError(
"External workspace is not set. Please call `set_external_workspace()`"
Expand All @@ -192,77 +200,33 @@ def _configure_external_workspace(self, **kwargs: Any) -> None:
if workspace is None:
raise RuntimeError(f"External workspace for device {device} not found.")
_validate_external_workspace(workspace, storage, required_nbytes)
self.sdfg_program.set_workspace(storage, workspace)

def construct_arguments(self, **kwargs: Any) -> None:
"""
This function will process the arguments and store the processed argument
vectors in `self.csdfg_args`, to call them use `self.fast_call()`.
"""
with dace.config.set_temporary("compiler", "allow_view_arguments", value=True):
csdfg_argv, csdfg_init_argv = self.sdfg_program.construct_arguments(**kwargs)
self._configure_external_workspace(**kwargs)
# Note we only care about `csdfg_argv` (normal call), since we have to update it,
# we ensure that it is a `list`.
self.csdfg_argv = [*csdfg_argv]
self.csdfg_init_argv = csdfg_init_argv

def fast_call(self) -> None:
"""
Perform a call to the compiled SDFG using the previously generated argument
vectors, see `self.construct_arguments()`.
"""
assert self.csdfg_argv is not None and self.csdfg_init_argv is not None, (
"Argument vector was not set properly."
)
self.sdfg_program.fast_call(
self.csdfg_argv, self.csdfg_init_argv, do_gpu_check=config.DEBUG
)
self.sdfg_program.set_workspace(storage, workspace, **kwargs)

def __call__(self, **kwargs: Any) -> None:
"""Call the compiled SDFG with the given arguments.

Note that this function will not update the argument vectors stored inside
`self`. Furthermore, it is not recommended to use this function as it is
very slow.
A `CompiledDaceProgram` should not be called directly. Instead
`gt4py.next.program_processors.runners.dace.workflow.decoration.convert_args()`
should be used to obtain a callable.
"""
warnings.warn(
"Called an SDFG through the standard DaCe interface is not recommended, use `fast_call()` instead.",
stacklevel=1,
raise NotImplementedError(
"A `CompiledDaceProgram` can not be called directly. Instead use "
"`gt4py.next.program_processors.runners.dace.workflow.decoration.convert_args()`."
)
result = self.sdfg_program(**kwargs)
assert result is None


@dataclasses.dataclass(frozen=True)
class DaCeCompilationArtifact:
"""Result of a DaCe compilation: library path + SDFG bindings + the SDFG itself.

The SDFG is carried inline as JSON because dace's load path
(``get_program_handle``) needs an SDFG instance to wrap into the
returned ``CompiledSDFG``, and the build folder may not contain a
``program.sdfg(z)`` dump under the upcoming minimal-build-dir mode.

The SDFG we store here is the one on which we called `SDFG.compile(return_program_handle=False)`.
Note that the `compile()` call has side effects, because it applies transformations
to the SDFG, in order to enable code generation for the target platform.
Since we pass `return_program_handle=False`, the `compile()` method does not
return a `CompiledSDFG` instance, therefore we cannot access `CompiledSDFG.sdfg`,
which would be the modified SDFG from which DaCe generates the C++/CUDA/HIP code.
"""
"""Result of a DaCe compilation: library path + SDFG bindings + the SDFG itself."""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
"""Result of a DaCe compilation: library path + SDFG bindings + the SDFG itself."""
"""Result of a DaCe compilation: SDFG build folder + SDFG bindings + the SDFG itself."""


library_path: pathlib.Path
sdfg_json: str
sdfg_build_folder: pathlib.Path
binding_source_code: str
bind_func_name: str
device_type: core_defs.DeviceType

def load(self) -> stages.ExecutableProgram:
# TODO(phimuell): Drop ``sdfg_json`` from the artifact once dace
# exposes a load path that doesn't require an SDFG instance to wrap
# into the returned ``CompiledSDFG``.
sdfg = dace.SDFG.from_json(json.loads(self.sdfg_json))
sdfg_program = dace_compiler.get_program_handle(self.library_path, sdfg)
sdfg_program = dace_compiler.load_precompiled_sdfg(self.sdfg_build_folder, sdfg=None)
sdfg_program.gpu_error_check = False # Not useful in asynchronous launches.
program = CompiledDaceProgram(sdfg_program, self.bind_func_name, self.binding_source_code)
return gtx_wfddecoration.DaCeDecoratedProgram(program, device_type=self.device_type)

Expand Down Expand Up @@ -290,7 +254,8 @@ class DaCeCompiler(
cmake_build_type: config.CMakeBuildType = dataclasses.field(
default_factory=lambda: config.CMAKE_BUILD_TYPE
)
# we store the non-default values of `dace.Config` in order to include it in the stage fingerprint
# We store the non-default values of `dace.Config` in order to include it in the stage fingerprint
# NOTE: They do not include the non default keys set through DaCe environment variables.
dace_config_nondefaults: dict[str, Any] = dataclasses.field(init=False)

def __post_init__(self) -> None:
Expand Down Expand Up @@ -323,8 +288,8 @@ def __call__(self, inp: SDFGExtensionSource) -> DaCeCompilationArtifact:
# Configure the SDFG build folder
sdfg.build_folder = sdfg_build_folder

# ``build_folder_mode`` is set by ``dace_context``; resolve the library
# path here so ``get_binary_name`` sees the same mode dace built under.
# `compiler.build_folder_mode` is set by `dace_context()`; resolve the library
# path here so `get_binary_name()` sees the same mode DaCe built under.
library_path = dace_compiler.get_binary_name(
object_folder=sdfg_build_folder, sdfg_name=sdfg.name
)
Expand All @@ -348,8 +313,7 @@ def __call__(self, inp: SDFGExtensionSource) -> DaCeCompilationArtifact:

assert inp.binding_source is not None
return DaCeCompilationArtifact(
library_path=library_path,
sdfg_json=json.dumps(inp.program_source.source_code),
sdfg_build_folder=sdfg_build_folder,
binding_source_code=inp.binding_source.source_code,
bind_func_name=self.bind_func_name,
device_type=self.device_type,
Expand Down
Loading
Loading