Skip to content
Open
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 pykokkos/core/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ def compile_object(
}:
raise Exception(f"Types are required for style: {entity.style}")

if self.is_compiled(module_setup.output_dir):
if self.is_compiled(module_setup.output_dir) and module_setup.is_compiled():
if hash not in self.members: # True if pre-compiled
if len(metadata) > 1:
entity, classtypes = self.fuse_objects(
Expand Down
21 changes: 20 additions & 1 deletion pykokkos/core/cpp_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,13 +201,32 @@ def generate_cmake(
if enable_uvm:
view_space = "Kokkos::Experimental::HIPManagedSpace"

# kernel compiled for host but default space is device
# in this case we need to convert them a bit
if is_host_execution_space(space):
default_space: ExecutionSpace = km.get_default_space()
if not is_host_execution_space(default_space):
ns = get_default_memory_space(default_space).name
prefix = "Kokkos::Experimental" if "HIP" in ns else "Kokkos"
view_space = f"{prefix}::{ns}"

space_value: str
if space.value == "HIP":
space_value = "Experimental::HIP"
else:
space_value = space.value

view_layout: str = str(get_default_layout(get_default_memory_space(space)))
# layout must also match the caller's views (GPU = LayoutLeft)
if is_host_execution_space(space):
default_space = km.get_default_space()
view_layout_space = (
default_space if not is_host_execution_space(default_space) else space
)
else:
view_layout_space = space
view_layout: str = str(
get_default_layout(get_default_memory_space(view_layout_space))
)
view_layout = view_layout.split(".")[-1]
view_layout = f"Kokkos::{view_layout}"

Expand Down
5 changes: 2 additions & 3 deletions pykokkos/core/fusion/fuse.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import ast
import os
from typing import Any, Dict, List, Set, Tuple, Union

from .util import DeclarationsVisitor, VariableRenamer
Expand Down Expand Up @@ -47,7 +46,7 @@ def fuse_workunit_kwargs_and_params(

for p in current_params:
current_arg = current_kwargs[p.arg]
if "PK_FUSE_ARGS" in os.environ and id(current_arg) in view_ids:
if id(current_arg) in view_ids:
continue

view_ids.add(id(current_arg))
Expand Down Expand Up @@ -131,7 +130,7 @@ def fuse_arguments(
continue

current_arg = current_kwargs[old_name]
if "PK_FUSE_ARGS" in os.environ and id(current_arg) in fused_view_names:
if id(current_arg) in fused_view_names:
name_map[key] = fused_view_names[id(current_arg)]
continue

Expand Down
1 change: 1 addition & 0 deletions pykokkos/core/keywords.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class Keywords(Enum):
ThreadsBegin = "pk_threads_begin"
ThreadsEnd = "pk_threads_end"
ArgMemSpace = "pk_arg_memspace"
ArgLayout = "pk_arg_layout"
DefaultExecSpace = "pk_exec_space"
DefaultExecSpaceInstance = "pk_exec_space_instance"
KernelName = "pk_kernel_name"
Expand Down
35 changes: 25 additions & 10 deletions pykokkos/core/module_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from typing import Callable, List, Optional, Set, Union

from pykokkos.interface import ExecutionSpace
from pykokkos.interface.execution_space import is_host_execution_space
import pykokkos.kokkos_manager as km

from .cpp_setup import CppSetup
Expand Down Expand Up @@ -181,7 +182,17 @@ def get_output_dir(
if restrict_signature is not None:
out_dir = out_dir / f"restrict_{restrict_signature}"

out_dir = out_dir / space.value
# special case:
# default space is device (all contexts are GPU-sided), but the kernel
# call is from host need to handle this differently, because
# `pk_arg_memspace/pk_arg_layout`s are GPU-oriented!!
dir_name: str = space.value
if is_host_execution_space(space):
default_space: ExecutionSpace = km.get_default_space()
if not is_host_execution_space(default_space):
dir_name = f"{space.value}_caller_{default_space.value}"

out_dir = out_dir / dir_name

return out_dir

Expand Down Expand Up @@ -250,15 +261,19 @@ def get_main_path(self) -> Path:

def is_compiled(self) -> bool:
"""
Check if this module is compiled for its execution space
Check if this module is compiled for its execution space.
Verifies the actual .so file exists, not just the directory, so that
a failed compilation (which leaves the directory but no .so) is
correctly detected as not-yet-compiled and retried.
"""

return CppSetup.is_compiled(
self.get_output_dir(
self.main,
self.metadata,
self.space,
self.types_signature,
self.restrict_signature,
)
output_dir: Optional[Path] = self.get_output_dir(
self.main,
self.metadata,
self.space,
self.types_signature,
self.restrict_signature,
)
if output_dir is None:
return False
return (output_dir / self.module_file).is_file()
48 changes: 43 additions & 5 deletions pykokkos/core/translators/bindings.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import sys
import re
from typing import Dict, List, Optional, Tuple

from pykokkos.core import cppast
Expand Down Expand Up @@ -82,7 +81,11 @@ def get_kernel_params(
continue

space: str = get_view_memory_space(t, "bindings")
layout: str = f"{Keywords.DefaultExecSpace.value}::array_layout"
# Use pk_arg_layout (set at compile time to match the caller's view
# layout) rather than pk_exec_space::array_layout. This lets a host
# execution space (Serial, OpenMP) accept GPU-layout views from a Cuda
# caller; create_mirror_view_and_copy inside run_* handles the copy.
layout: str = Keywords.ArgLayout.value
params[n.declname] = cpp_view_type(t, space=space, layout=layout, real=real)

params[Keywords.DefaultExecSpaceInstance.value] = Keywords.DefaultExecSpace.value
Expand Down Expand Up @@ -139,6 +142,37 @@ def get_device_views(members: PyKokkosMembers) -> Dict[str, str]:
}


def _generate_mirror_with_exec_layout(
src: str,
dst: str,
view_type: cppast.ClassType,
real: Optional[str],
) -> str:
"""
Generate C++ code that creates a properly-typed mirror view in the
execution space's native memory space AND layout, then deep-copies from
the source view.
"""
# build destination view type with specified memory and layout
exec_space: str = Keywords.DefaultExecSpace.value
dst_type: str = cpp_view_type(
view_type,
space=f"{exec_space}::memory_space",
layout=f"{exec_space}::array_layout",
real=real,
)

rank: int = visitors_util.get_view_rank_from_typename(view_type.typename)
extents: str = ",".join(f"{src}.extent({i})" for i in range(rank))

code: str = (
f"{dst_type} {dst}("
f'Kokkos::view_alloc("{dst}", Kokkos::WithoutInitializing), {extents});'
f"Kokkos::deep_copy({dst}, {src});"
)
return code


def generate_functor_instance(
functor: str,
members: PyKokkosMembers,
Expand Down Expand Up @@ -180,11 +214,15 @@ def generate_functor_instance(
get_view_memory_space(view_type, "bindings")
== Keywords.ArgMemSpace.value
):
mirror_views += f"auto {d_v} = Kokkos::create_mirror_view_and_copy({exec_space_instance}, {v});"
mirror_views += _generate_mirror_with_exec_layout(
v, d_v, view_type, None
)
else:
mirror_views += f"auto {d_v} = {v};"
else:
mirror_views += f"auto {d_v} = Kokkos::create_mirror_view_and_copy({exec_space_instance}, {v});"
mirror_views += _generate_mirror_with_exec_layout(
v, d_v, members.views[cppast.DeclRefExpr(v)], None
)

# Kokkos fails to compile a functor if there are no parameters in its constructor
if len(args) == 0:
Expand Down Expand Up @@ -225,7 +263,7 @@ def generate_copy_back_from_dict(

# Need to resize views for binsort. Unmanaged views cannot be resized.
if cppast.DeclRefExpr("Unmanaged") not in view_type.template_params:
rank = int(re.search(r"\d+", view_type.typename).group())
rank = visitors_util.get_view_rank_from_typename(view_type.typename)
resize_args: List[str] = [v]

for i in range(rank):
Expand Down
9 changes: 9 additions & 0 deletions pykokkos/core/visitors/constructor_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,15 @@ def visit_AnnAssign(
if not isinstance(node.annotation, ast.Subscript):
return ()

if (
node.value is not None
and isinstance(node.value, ast.Subscript)
and isinstance(node.value.value, ast.Attribute)
and isinstance(node.value.value.value, ast.Name)
and node.value.value.value.id == "self"
):
return (declref, None)

decltype: cppast.ClassType = visitors_util.get_type(
node.annotation, self.pk_import
)
Expand Down
45 changes: 29 additions & 16 deletions pykokkos/core/visitors/visitors_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,34 @@ def get_type(
return None


def get_view_rank_from_typename(typename: str) -> int:
"""
Extract view rank from a view type name (e.g. 'View2D', 'ScratchView3D').

:param typename: view type string such as 'View1D', 'ScratchView2D'
:returns: rank as int (1-7)
:raises ValueError: if typename is not a valid view type name
"""
if typename.startswith("ScratchView"):
suffix = typename[len("ScratchView") :]
elif typename.startswith("View"):
suffix = typename[len("View") :]
else:
raise ValueError(
f"Expected a view type (e.g., 'View1D', 'View2D', 'ScratchView1D'), "
f"but got '{typename}'."
)
if not suffix.endswith("D") or len(suffix) < 2:
raise ValueError(
f"Could not extract view rank from typename '{typename}'. "
f"Expected format: 'View<rank>D' or 'ScratchView<rank>D' (e.g., 'View1D', 'View2D')"
)
rank = int(suffix[:-1])
if not 0 < rank < 8:
raise ValueError(f"View rank {rank} is not allowed")
return rank


def parse_view_template_params(
view_type: cppast.ClassType,
rank: Optional[int] = None,
Expand Down Expand Up @@ -308,22 +336,7 @@ def parse_view_template_params(
)

if rank is None:
# Match the rank number that comes after "View" or "ScratchView" and before "D"
# This prevents matching numbers from dtype names like "float32" or "float64"
match = re.search(r"(?:View|ScratchView)(\d+)D", py_type)
if match:
rank = int(match.group(1))
else:
# If pattern doesn't match, this is likely not a valid view type name
# or the typename format is unexpected - raise an error instead of
# using a fallback that could match wrong numbers from dtype names
raise ValueError(
f"Could not extract view rank from typename '{py_type}'. "
f"Expected format: 'View<rank>D' or 'ScratchView<rank>D' (e.g., 'View1D', 'View2D')"
)

if not 0 < rank < 8:
raise ValueError(f"View rank {rank} is not allowed")
rank = get_view_rank_from_typename(py_type)

params: Dict[str, str] = {}

Expand Down
Loading