• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

daisytuner / docc / 29726336683

20 Jul 2026 07:59AM UTC coverage: 63.437% (+0.3%) from 63.18%
29726336683

Pull #857

github

web-flow
Merge 129cc05f2 into b57f184e7
Pull Request #857: Added PyTorch frontend

419 of 544 new or added lines in 14 files covered. (77.02%)

1 existing line in 1 file now uncovered.

41006 of 64641 relevant lines covered (63.44%)

977.69 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

61.89
/python/docc/compiler/compiled_sdfg.py
1
import ctypes
4✔
2
import re
4✔
3
import time
4✔
4
import warnings
4✔
5
from docc.sdfg import Scalar, Array, Pointer, Structure, PrimitiveType
4✔
6

7
import numpy as np
4✔
8
import ml_dtypes
4✔
9

10

11
class DoccPerformanceWarning(UserWarning):
4✔
12
    """Warning emitted when a slower-than-necessary execution path is taken."""
13

14

15
def warn_device_residency_failed(backend):
4✔
16
    """Emit a one-time warning that device-residency promotion did not apply.
17

18
    Args:
19
        backend: The GPU backend name (e.g. "cuda", "rocm") for context.
20
    """
21
    warnings.warn(
×
22
        f"Device residency could not be enabled for this function on backend {backend}. "
23
        f"The function is only partially offloaded; function arguments stay in host memory before "
24
        f"and after execution.",
25
        DoccPerformanceWarning,
26
        stacklevel=3,
27
    )
28

29

30
def warn_host_to_device_copies(backend):
4✔
31
    """Emit a one-time performance warning about unnecessary host-to-device copies.
32

33
    Args:
34
        backend: The GPU backend name (e.g. "cuda", "rocm") for context.
35
    """
36
    warnings.warn(
×
37
        f"Device residency is enabled for this function on backend {backend}. "
38
        f"Function arguments are passed from host memory slowing down execution. "
39
        f"Provide device-resident arrays to avoid unnecessary copies and improve performance.",
40
        DoccPerformanceWarning,
41
        stacklevel=3,
42
    )
43

44

45
def idiv(a, b):
4✔
46
    """Integer division (floor division for positive numbers)."""
47
    return int(a) // int(b)
4✔
48

49

50
def _is_device_array(arg):
4✔
51
    """Return True if ``arg`` already lives in device memory.
52

53
    cupy arrays and CUDA torch tensors expose ``__cuda_array_interface__``; a
54
    torch tensor reports its location via ``is_cuda``. Host arrays (numpy, CPU
55
    torch tensors) return False.
56
    """
57
    is_cuda = getattr(arg, "is_cuda", None)
×
58
    if is_cuda is not None:
×
59
        return bool(is_cuda)
×
60
    if getattr(arg, "__cuda_array_interface__", None) is not None:
×
61
        return True
×
62
    return False
×
63

64

65
def _device_array_ptr(arg):
4✔
66
    """Extract the raw device pointer from a GPU array (cupy or torch.cuda).
67

68
    Both cupy arrays and CUDA torch tensors expose ``__cuda_array_interface__``;
69
    torch tensors additionally expose ``data_ptr()``. Returns an integer address.
70
    """
71
    data_ptr = getattr(arg, "data_ptr", None)
×
72
    if callable(data_ptr):
×
73
        return data_ptr()
×
74
    cai = getattr(arg, "__cuda_array_interface__", None)
×
75
    if cai is not None:
×
76
        return cai["data"][0]
×
77
    raise TypeError(
×
78
        f"Device-resident execution requires a GPU array exposing "
79
        f"__cuda_array_interface__ or data_ptr(), got {type(arg).__name__}"
80
    )
81

82

83
# Evaluation context for shape expressions
84
_EVAL_GLOBALS = {"idiv": idiv}
4✔
85

86
# Pre-compiled regex for _convert_to_python_syntax
87
_FUNC_CALL_PATTERN = re.compile(r"([a-zA-Z_][a-zA-Z0-9_]*)\(([^()]+)\)")
4✔
88
_PLACEHOLDER_PATTERN = re.compile(
4✔
89
    r"@@@FUNC@@@([a-zA-Z_][a-zA-Z0-9_]*)@@@(.+?)@@@END@@@"
90
)
91
_KNOWN_FUNCTIONS = frozenset(
4✔
92
    {"int", "float", "abs", "min", "max", "sum", "len", "idiv"}
93
)
94

95
# Argument type constants for fast dispatch
96
_ARG_TYPE_OUTPUT_ARRAY = 0
4✔
97
_ARG_TYPE_OUTPUT_SCALAR = 1
4✔
98
_ARG_TYPE_SHAPE = 2
4✔
99
_ARG_TYPE_USER_ARRAY = 3
4✔
100
_ARG_TYPE_USER_STRUCT = 4
4✔
101
_ARG_TYPE_USER_SCALAR = 5
4✔
102

103
# Call modes. A single call uses exactly one mode (mixing array kinds is not
104
# allowed). The mode, combined with whether the artifact is device-resident,
105
# fully determines the execution path:
106
#
107
#   mode      | non-device-resident        | device-resident
108
#   ----------|----------------------------|-------------------------------
109
#   NumPyCPU  | host execution (default)   | host->device copy + warning
110
#   NumPyGPU  | rejected (TypeError)       | zero-copy device execution
111
#   TorchCPU  | host execution             | host->device copy + warning
112
#   TorchGPU  | rejected (TypeError)       | zero-copy device execution
113
_CALL_MODE_NUMPY_CPU = "NumPyCPU"
4✔
114
_CALL_MODE_NUMPY_GPU = "NumPyGPU"
4✔
115
_CALL_MODE_TORCH_CPU = "TorchCPU"
4✔
116
_CALL_MODE_TORCH_GPU = "TorchGPU"
4✔
117

118
# Modes whose data lives in device memory; these require a device-resident
119
# artifact, otherwise the call is rejected.
120
_GPU_CALL_MODES = frozenset({_CALL_MODE_NUMPY_GPU, _CALL_MODE_TORCH_GPU})
4✔
121

122

123
def _is_torch_tensor(arg):
4✔
124
    """Return True if ``arg`` is a torch tensor or Parameter (without importing torch)."""
125
    return any(
4✔
126
        cls.__module__.startswith("torch") and cls.__name__ in ("Tensor", "Parameter")
127
        for cls in type(arg).__mro__
128
    )
129

130

131
def _classify_array_kind(arg):
4✔
132
    """Classify a single argument into one of the four call modes."""
133
    if _is_torch_tensor(arg):
4✔
134
        return (
×
135
            _CALL_MODE_TORCH_GPU
136
            if getattr(arg, "is_cuda", False)
137
            else _CALL_MODE_TORCH_CPU
138
        )
139
    root = type(arg).__module__.split(".", 1)[0]
4✔
140
    if root == "cupy":
4✔
141
        return _CALL_MODE_NUMPY_GPU
×
142
    if isinstance(arg, np.ndarray):
4✔
143
        return _CALL_MODE_NUMPY_CPU
4✔
144
    # Any other object exposing the CUDA array interface is a device array.
145

146
    if getattr(arg, "__cuda_array_interface__", None) is not None:
4✔
147
        return _CALL_MODE_NUMPY_GPU
×
148
    return None
4✔
149

150

151
# Pre-cache ctypes.c_int64 for speed
152
_c_int64 = ctypes.c_int64
4✔
153
_ctypes_cast = ctypes.cast
4✔
154
_ctypes_addressof = ctypes.addressof
4✔
155
_ctypes_byref = ctypes.byref
4✔
156
_ctypes_pointer = ctypes.pointer
4✔
157
_ctypes_c_void_p = ctypes.c_void_p
4✔
158

159
# Map primitive types to numpy dtypes for fast buffer allocation
160
_PRIMITIVE_TO_NP_DTYPE = {
4✔
161
    PrimitiveType.Float: np.float32,
162
    PrimitiveType.Double: np.float64,
163
    PrimitiveType.Int8: np.int8,
164
    PrimitiveType.Int16: np.int16,
165
    PrimitiveType.Int32: np.int32,
166
    PrimitiveType.Int64: np.int64,
167
    PrimitiveType.UInt8: np.uint8,
168
    PrimitiveType.UInt16: np.uint16,
169
    PrimitiveType.UInt32: np.uint32,
170
    PrimitiveType.UInt64: np.uint64,
171
    PrimitiveType.Bool: np.bool_,
172
    PrimitiveType.Half: np.float16,
173
    PrimitiveType.BFloat: ml_dtypes.bfloat16,
174
}
175

176
# Pre-computed dtype map for numpy conversion
177
_NUMPY_DTYPE_MAP = {
4✔
178
    PrimitiveType.Float: np.float32,
179
    PrimitiveType.Double: np.float64,
180
    PrimitiveType.Int8: np.int8,
181
    PrimitiveType.Int16: np.int16,
182
    PrimitiveType.Int32: np.int32,
183
    PrimitiveType.Int64: np.int64,
184
    PrimitiveType.UInt8: np.uint8,
185
    PrimitiveType.UInt16: np.uint16,
186
    PrimitiveType.UInt32: np.uint32,
187
    PrimitiveType.UInt64: np.uint64,
188
    PrimitiveType.Bool: np.bool_,
189
    PrimitiveType.Half: np.float16,
190
    PrimitiveType.BFloat: ml_dtypes.bfloat16,
191
}
192

193
_CTYPES_MAP = {
4✔
194
    PrimitiveType.Bool: ctypes.c_bool,
195
    PrimitiveType.Int8: ctypes.c_int8,
196
    PrimitiveType.Int16: ctypes.c_int16,
197
    PrimitiveType.Int32: ctypes.c_int32,
198
    PrimitiveType.Int64: ctypes.c_int64,
199
    PrimitiveType.UInt8: ctypes.c_uint8,
200
    PrimitiveType.UInt16: ctypes.c_uint16,
201
    PrimitiveType.UInt32: ctypes.c_uint32,
202
    PrimitiveType.UInt64: ctypes.c_uint64,
203
    PrimitiveType.Float: ctypes.c_float,
204
    PrimitiveType.Double: ctypes.c_double,
205
    # Half and BFloat are 2 bytes, use c_uint16 for raw storage
206
    PrimitiveType.Half: ctypes.c_uint16,
207
    PrimitiveType.BFloat: ctypes.c_uint16,
208
}
209

210

211
class CompiledSDFG:
4✔
212
    def __init__(
4✔
213
        self,
214
        lib_path,
215
        sdfg,
216
        shape_sources=None,
217
        structure_member_info=None,
218
        output_args=None,
219
        output_shapes=None,
220
        output_strides=None,
221
        device_resident=False,
222
        device_backend=None,
223
        target=None,
224
        sort_output_args=True,
225
    ):
226
        self.lib_path = lib_path
4✔
227
        self.sdfg = sdfg
4✔
228
        self.shape_sources = shape_sources or []
4✔
229
        self.structure_member_info = structure_member_info or {}
4✔
230
        self.lib = ctypes.CDLL(lib_path)
4✔
231
        self.func = getattr(self.lib, sdfg.name)
4✔
232

233
        # Check for output args
234
        self.output_args = output_args or []
4✔
235
        if not self.output_args and hasattr(sdfg, "metadata"):
4✔
236
            out_args_str = sdfg.metadata("output_args")
4✔
237
            if out_args_str:
4✔
238
                self.output_args = out_args_str.split(",")
×
239

240
        self.output_shapes = output_shapes or {}
4✔
241
        self.output_strides = output_strides or {}
4✔
242

243
        # Device residency: set by the DeviceResidentArgPromotion pass when all
244
        # pointer arguments were promoted to device-resident storage. When active,
245
        # the compiled function expects device pointers (no host<->device copies at
246
        # the boundary) and produces device-resident outputs. Communicated
247
        # explicitly via the constructor (pass return value), not via metadata.
248
        self.device_resident = bool(device_resident)
4✔
249
        self.device_backend = device_backend or (
4✔
250
            "cuda" if self.device_resident else None
251
        )
252
        # Warn at most once per artifact when host inputs must be copied to device.
253
        self._warned_host_to_device = False
4✔
254

255
        # Compilation target (e.g. "cuda"/"rocm"/"sequential"). Used to inform,
256
        # once, when a GPU target ran on the host because device-residency
257
        # promotion did not apply to this artifact.
258
        self.target = target
4✔
259
        self._warned_residency_failed = False
4✔
260

261
        # Whether to sort the output arguments or not. Default is True. This has to be disable for
262
        # frontends that do not call there output arguments _docc_ret_[i] where [i] is the index.
263
        # Those frontends have to ensure the correct ordering themselves.
264
        self._sort_output_args = sort_output_args
4✔
265

266
        # Cache for ctypes structure definitions
267
        self._ctypes_structures = {}
4✔
268

269
        # Set up argument types
270
        self.arg_names = sdfg.arguments
4✔
271
        self.arg_types = []
4✔
272
        self.arg_sdfg_types = []  # Keep track of original sdfg types
4✔
273
        for arg_name in sdfg.arguments:
4✔
274
            arg_type = sdfg.type(arg_name)
4✔
275
            self.arg_sdfg_types.append(arg_type)
4✔
276
            ct_type = self._get_ctypes_type(arg_type)
4✔
277
            self.arg_types.append(ct_type)
4✔
278

279
        self.func.argtypes = self.arg_types
4✔
280

281
        # Set up return type
282
        self.func.restype = self._get_ctypes_type(sdfg.return_type)
4✔
283

284
        # Pre-compute argument classification for fast __call__
285
        self._precompute_arg_metadata()
4✔
286

287
    def _precompute_arg_metadata(self):
4✔
288
        """Pre-compute argument metadata for fast __call__ dispatch."""
289
        output_args_set = set(self.output_args)
4✔
290

291
        # Build shape source lookup: s_idx -> (u_idx, dim_idx)
292
        # Also pre-compute the shape keys
293
        self._shape_sources_list = []  # [(s_idx, u_idx, dim_idx, key_str), ...]
4✔
294
        for i, (u_idx, dim_idx) in enumerate(self.shape_sources):
4✔
295
            self._shape_sources_list.append((i, u_idx, dim_idx, f"_s{i}"))
4✔
296

297
        # Classify each argument using tuple-based info for faster access
298
        # Each entry is (arg_type, *type_specific_data)
299
        self._arg_info = []
4✔
300
        user_arg_counter = 0
4✔
301

302
        # For output ordering (avoid sorting at runtime)
303
        output_order = []
4✔
304

305
        for i, arg_name in enumerate(self.arg_names):
4✔
306
            if arg_name in output_args_set:
4✔
307
                # Output argument
308
                target_type = self.arg_types[i]
4✔
309
                base_type = target_type._type_
4✔
310
                sdfg_type = self.arg_sdfg_types[i]
4✔
311

312
                # Pre-compute primitive type for return processing
313
                primitive_type = None
4✔
314
                if isinstance(sdfg_type, Pointer) and sdfg_type.has_pointee_type():
4✔
315
                    pointee = sdfg_type.pointee_type
4✔
316
                    if isinstance(pointee, Scalar):
4✔
317
                        primitive_type = pointee.primitive_type
4✔
318

319
                if arg_name in self.output_shapes:
4✔
320
                    dims = self.output_shapes[arg_name]
4✔
321
                    # Always compile shape expressions - they may depend on runtime values
322
                    compiled_dims = []
4✔
323
                    for d in dims:
4✔
324
                        d_str = str(d)
4✔
325
                        expr = self._convert_to_python_syntax(d_str)
4✔
326
                        compiled_dims.append(compile(expr, "<shape>", "eval"))
4✔
327

328
                    # Pre-compile stride expressions if available
329
                    compiled_strides = None
4✔
330
                    if arg_name in self.output_strides:
4✔
331
                        compiled_strides = []
4✔
332
                        for s in self.output_strides[arg_name]:
4✔
333
                            expr = self._convert_to_python_syntax(str(s))
4✔
334
                            compiled_strides.append(compile(expr, "<stride>", "eval"))
4✔
335

336
                    # Get numpy dtype for fast allocation
337
                    np_dtype = (
4✔
338
                        _PRIMITIVE_TO_NP_DTYPE.get(primitive_type, np.float64)
339
                        if primitive_type
340
                        else np.float64
341
                    )
342

343
                    # Tuple: (arg_type, name, base_type, target_type, compiled_dims, compiled_strides, primitive_type, np_dtype)
344
                    info_idx = len(self._arg_info)
4✔
345
                    self._arg_info.append(
4✔
346
                        (
347
                            _ARG_TYPE_OUTPUT_ARRAY,
348
                            arg_name,
349
                            base_type,
350
                            target_type,
351
                            compiled_dims,
352
                            compiled_strides,
353
                            primitive_type,
354
                            np_dtype,
355
                        )
356
                    )
357
                    if self._sort_output_args:
4✔
358
                        output_order.append((int(arg_name.split("_")[-1]), info_idx))
4✔
359
                    else:
NEW
360
                        output_order.append(info_idx)
×
361
                else:
362
                    # Scalar return
363
                    info_idx = len(self._arg_info)
4✔
364
                    self._arg_info.append(
4✔
365
                        (_ARG_TYPE_OUTPUT_SCALAR, arg_name, base_type, primitive_type)
366
                    )
367
                    if self._sort_output_args:
4✔
368
                        output_order.append((int(arg_name.split("_")[-1]), info_idx))
4✔
369
                    else:
NEW
370
                        output_order.append(info_idx)
×
371

372
            elif arg_name.startswith("_s") and arg_name[2:].isdigit():
4✔
373
                # Shape symbol argument - tuple: (arg_type, s_idx, key_str)
374
                s_idx = int(arg_name[2:])
4✔
375
                self._arg_info.append((_ARG_TYPE_SHAPE, s_idx, f"_s{s_idx}"))
4✔
376
            else:
377
                # User argument
378
                sdfg_type = self.arg_sdfg_types[i]
4✔
379
                target_type = self.arg_types[i]
4✔
380
                is_struct_ptr = (
4✔
381
                    sdfg_type
382
                    and isinstance(sdfg_type, Pointer)
383
                    and sdfg_type.has_pointee_type()
384
                    and isinstance(sdfg_type.pointee_type, Structure)
385
                )
386

387
                if is_struct_ptr:
4✔
388
                    struct_name = sdfg_type.pointee_type.name
4✔
389
                    struct_class = self._create_ctypes_structure(struct_name)
4✔
390
                    members = self.structure_member_info[struct_name]
4✔
391
                    sorted_members = tuple(
4✔
392
                        sorted(members.items(), key=lambda x: x[1][0])
393
                    )
394
                    # Tuple: (arg_type, user_idx, name, struct_class, sorted_members)
395
                    self._arg_info.append(
4✔
396
                        (
397
                            _ARG_TYPE_USER_STRUCT,
398
                            user_arg_counter,
399
                            arg_name,
400
                            struct_class,
401
                            sorted_members,
402
                        )
403
                    )
404
                elif hasattr(target_type, "contents"):
4✔
405
                    # Array user arg - tuple: (arg_type, user_idx, name, target_type)
406
                    self._arg_info.append(
4✔
407
                        (_ARG_TYPE_USER_ARRAY, user_arg_counter, arg_name, target_type)
408
                    )
409
                else:
410
                    # Scalar user arg - tuple: (arg_type, user_idx, name, target_type)
411
                    self._arg_info.append(
4✔
412
                        (_ARG_TYPE_USER_SCALAR, user_arg_counter, arg_name, target_type)
413
                    )
414
                user_arg_counter += 1
4✔
415

416
        self._num_user_args = user_arg_counter
4✔
417

418
        if self._sort_output_args:
4✔
419
            # Pre-sort output order and build position map
420
            output_order.sort(key=lambda x: x[0])
4✔
421
            self._output_order = tuple(idx for _, idx in output_order)
4✔
422
        else:
NEW
423
            self._output_order = output_order
×
424

425
        # Map from _arg_info index to result position (for O(1) lookup)
426
        self._output_pos_map = {idx: pos for pos, idx in enumerate(self._output_order)}
4✔
427

428
    def _convert_to_python_syntax(self, expr_str):
4✔
429
        result = expr_str
4✔
430

431
        while True:
4✔
432
            match = _FUNC_CALL_PATTERN.search(result)
4✔
433
            if not match:
4✔
434
                break
4✔
435

436
            name = match.group(1)
4✔
437
            index = match.group(2)
4✔
438

439
            if name.lower() in _KNOWN_FUNCTIONS:
4✔
440
                # Use unique delimiters that won't appear in expressions
441
                placeholder = f"@@@FUNC@@@{name}@@@{index}@@@END@@@"
4✔
442
                result = result[: match.start()] + placeholder + result[match.end() :]
4✔
443
            else:
444
                result = (
×
445
                    result[: match.start()] + f"{name}[{index}]" + result[match.end() :]
446
                )
447

448
        result = _PLACEHOLDER_PATTERN.sub(r"\1(\2)", result)
4✔
449

450
        return result
4✔
451

452
    def _create_ctypes_structure(self, struct_name):
4✔
453
        """Create a ctypes Structure class for the given structure name."""
454
        if struct_name in self._ctypes_structures:
4✔
455
            return self._ctypes_structures[struct_name]
4✔
456

457
        if struct_name not in self.structure_member_info:
4✔
458
            raise ValueError(f"Structure '{struct_name}' not found in member info")
×
459

460
        # Get member info: {member_name: (index, type)}
461
        members = self.structure_member_info[struct_name]
4✔
462
        # Sort by index to get correct order
463
        sorted_members = sorted(members.items(), key=lambda x: x[1][0])
4✔
464

465
        # Build _fields_ for ctypes.Structure
466
        fields = []
4✔
467
        for member_name, member_info in sorted_members:
4✔
468
            member_type = member_info[1]
4✔
469
            ct_type = self._get_ctypes_type(member_type)
4✔
470
            fields.append((member_name, ct_type))
4✔
471

472
        # Create the ctypes Structure class dynamically
473
        class CStructure(ctypes.Structure):
4✔
474
            _fields_ = fields
4✔
475

476
        self._ctypes_structures[struct_name] = CStructure
4✔
477
        return CStructure
4✔
478

479
    def _get_ctypes_type(self, sdfg_type):
4✔
480
        if isinstance(sdfg_type, Scalar):
4✔
481
            return _CTYPES_MAP.get(sdfg_type.primitive_type, ctypes.c_void_p)
4✔
482
        elif isinstance(sdfg_type, Array):
4✔
483
            # Arrays are passed as pointers
484
            elem_type = _CTYPES_MAP.get(sdfg_type.primitive_type, ctypes.c_void_p)
×
485
            return ctypes.POINTER(elem_type)
×
486
        elif isinstance(sdfg_type, Pointer):
4✔
487
            # Check if pointee is a Structure
488
            # Note: has_pointee_type() is guaranteed to exist on Pointer instances from C++ bindings
489
            if sdfg_type.has_pointee_type():
4✔
490
                pointee = sdfg_type.pointee_type
4✔
491
                if isinstance(pointee, Structure):
4✔
492
                    # Create ctypes structure and return pointer to it
493
                    struct_class = self._create_ctypes_structure(pointee.name)
4✔
494
                    return ctypes.POINTER(struct_class)
4✔
495
                elif isinstance(pointee, Scalar):
4✔
496
                    elem_type = _CTYPES_MAP.get(pointee.primitive_type, ctypes.c_void_p)
4✔
497
                    return ctypes.POINTER(elem_type)
4✔
498
            return ctypes.c_void_p
×
499
        return ctypes.c_void_p
×
500

501
    def _convert_return_value(self, func_result, shape_symbol_values):
4✔
502
        return_type = self.sdfg.return_type
4✔
503

504
        if isinstance(return_type, Pointer):
4✔
505
            if return_type.has_pointee_type():
×
506
                pointee = return_type.pointee_type
×
507
                if isinstance(pointee, Scalar):
×
508
                    # Pointer to scalar element type - need to determine array size
509
                    # Get return shape from metadata if available
510
                    return_shape_str = self.sdfg.metadata("return_shape")
×
511
                    if return_shape_str:
×
512
                        # Strip brackets (metadata may be "[10,10]" format)
513
                        return_shape_str = return_shape_str.strip("[]")
×
514
                        shape = []
×
515
                        for dim_str in return_shape_str.split(","):
×
516
                            try:
×
517
                                eval_str = self._convert_to_python_syntax(str(dim_str))
×
518
                                val = eval(eval_str, _EVAL_GLOBALS, shape_symbol_values)
×
519
                                shape.append(int(val))
×
520
                            except Exception:
×
521
                                # Can't evaluate shape, return raw pointer
522
                                return func_result
×
523

524
                        # Determine numpy dtype from primitive type
525
                        dtype = _NUMPY_DTYPE_MAP.get(pointee.primitive_type, np.float64)
×
526

527
                        # Calculate total size
528
                        total_size = 1
×
529
                        for dim in shape:
×
530
                            total_size *= dim
×
531

532
                        # Create numpy array from pointer
533
                        ct_type = _CTYPES_MAP.get(
×
534
                            pointee.primitive_type, ctypes.c_double
535
                        )
536
                        arr_type = ct_type * total_size
×
537
                        # For Half/BFloat, use np.frombuffer since np.ctypeslib.as_array
538
                        # doesn't support these types (PEP 3118 buffer format limitation)
539
                        if pointee.primitive_type in (
×
540
                            PrimitiveType.Half,
541
                            PrimitiveType.BFloat,
542
                        ):
543
                            byte_size = total_size * 2  # Half and BFloat are 2 bytes
×
544
                            arr = np.frombuffer(
×
545
                                (ctypes.c_char * byte_size).from_address(
546
                                    ctypes.cast(func_result, ctypes.c_void_p).value
547
                                ),
548
                                dtype=dtype,
549
                            ).copy()
550
                        else:
551
                            arr = np.ctypeslib.as_array(
×
552
                                ctypes.cast(
553
                                    func_result, ctypes.POINTER(arr_type)
554
                                ).contents
555
                            )
556
                        return arr.reshape(shape)
×
557
                    else:
558
                        # No shape info - try to infer from input shapes
559
                        # For identity-like operations, the output shape matches input
560
                        if len(self.shape_sources) > 0 and len(shape_symbol_values) > 0:
×
561
                            # Use first input's shape as a fallback
562
                            shape = []
×
563
                            for i in range(len(self.shape_sources)):
×
564
                                if f"_s{i}" in shape_symbol_values:
×
565
                                    shape.append(shape_symbol_values[f"_s{i}"])
×
566

567
                            if shape:
×
568
                                dtype = _NUMPY_DTYPE_MAP.get(
×
569
                                    pointee.primitive_type, np.float64
570
                                )
571

572
                                total_size = 1
×
573
                                for dim in shape:
×
574
                                    total_size *= dim
×
575

576
                                ct_type = _CTYPES_MAP.get(
×
577
                                    pointee.primitive_type, ctypes.c_double
578
                                )
579
                                arr_type = ct_type * total_size
×
580
                                # For Half/BFloat, use np.frombuffer since np.ctypeslib.as_array
581
                                # doesn't support these types (PEP 3118 buffer format limitation)
582
                                if pointee.primitive_type in (
×
583
                                    PrimitiveType.Half,
584
                                    PrimitiveType.BFloat,
585
                                ):
586
                                    byte_size = (
×
587
                                        total_size * 2
588
                                    )  # Half and BFloat are 2 bytes
589
                                    arr = np.frombuffer(
×
590
                                        (ctypes.c_char * byte_size).from_address(
591
                                            ctypes.cast(
592
                                                func_result, ctypes.c_void_p
593
                                            ).value
594
                                        ),
595
                                        dtype=dtype,
596
                                    ).copy()
597
                                else:
598
                                    arr = np.ctypeslib.as_array(
×
599
                                        ctypes.cast(
600
                                            func_result, ctypes.POINTER(arr_type)
601
                                        ).contents
602
                                    )
603
                                return arr.reshape(shape)
×
604

605
                        # Can't determine shape, return raw pointer
606
                        return func_result
×
607
        elif isinstance(return_type, Scalar):
4✔
608
            return func_result
4✔
609

610
        return func_result
×
611

612
    def _ensure_device_array(self, arg, keepalive, writebacks):
4✔
613
        """Return a device array for ``arg``, copying host inputs to the device.
614

615
        Device-resident artifacts consume raw device pointers. For backwards
616
        compatibility, callers may still pass host arrays (numpy arrays or CPU
617
        torch tensors); these are copied to the device here and a one-time
618
        performance warning is emitted. Device arrays are returned unchanged.
619

620
        The copied device array is appended to ``keepalive`` so it outlives the
621
        call, and the ``(host, device)`` pair is recorded in ``writebacks`` so
622
        that in-place writes (e.g. output arguments) are mirrored back to the
623
        original host array after execution.
624
        """
625
        if _is_device_array(arg):
×
626
            return arg
×
627

628
        import cupy
×
629

630
        host = arg
×
631
        # Convert a CPU torch tensor to numpy first; cupy.asarray handles numpy.
632
        if hasattr(host, "detach") and hasattr(host, "numpy"):
×
633
            host = host.detach().numpy()
×
634
        device_arg = cupy.asarray(host)
×
635
        keepalive.append(device_arg)
×
636
        writebacks.append((arg, device_arg))
×
637

638
        if not self._warned_host_to_device:
×
639
            self._warned_host_to_device = True
×
640
            warn_host_to_device_copies(self.device_backend or "cuda")
×
641

642
        return device_arg
×
643

644
    def _call_device(self, *args):
4✔
645
        """Execute with device-resident arguments (cupy / torch.cuda).
646

647
        Inputs are passed as raw device pointers and output arrays are allocated
648
        on the device, so no host<->device copies happen at the call boundary.
649
        Outputs are returned as cupy arrays (zero-copy interoperable with torch
650
        via DLPack / __cuda_array_interface__).
651
        """
652
        import cupy
×
653

654
        _eval = eval
×
655
        _GLOBALS = _EVAL_GLOBALS
×
656
        # 1. Build shape_symbol_values from shape sources
657
        shape_symbol_values = {}
×
658
        for s_idx, u_idx, dim_idx, key in self._shape_sources_list:
×
659
            if u_idx < len(args):
×
660
                shape_symbol_values[key] = args[u_idx].shape[dim_idx]
×
661

662
        # 2. Process arguments
663
        converted_args = []
×
664
        keepalive = []  # keep device buffers / ctypes scalars alive
×
665
        writebacks = []  # (host_arg, device_arg) for host inputs copied to device
×
666
        return_buffers = []  # (buf, size, dims, compiled_strides, primitive_type)
×
667
        # Track whether the caller supplied device arrays. When all array inputs
668
        # are host arrays (numpy / CPU torch), the caller works in host space and
669
        # expects host outputs, so device output buffers are copied back to host.
670
        any_device_input = False
×
671

672
        for info in self._arg_info:
×
673
            arg_type = info[0]
×
674

675
            if arg_type == _ARG_TYPE_OUTPUT_ARRAY:
×
676
                target_type = info[3]
×
677
                compiled_dims = info[4]
×
678
                compiled_strides = info[5]
×
679
                np_dtype = info[7]
×
680

681
                size = 1
×
682
                dims = []
×
683
                for code in compiled_dims:
×
684
                    d = int(_eval(code, _GLOBALS, shape_symbol_values))
×
685
                    dims.append(d)
×
686
                    size *= d
×
687

688
                buf = cupy.empty(size, dtype=np_dtype)
×
689
                keepalive.append(buf)
×
690
                return_buffers.append((buf, size, dims, compiled_strides, info[6]))
×
691
                converted_args.append(_ctypes_cast(int(buf.data.ptr), target_type))
×
692

693
            elif arg_type == _ARG_TYPE_OUTPUT_SCALAR:
×
694
                base_type = info[2]
×
695
                primitive_type = info[3]
×
696
                buf = base_type()
×
697
                keepalive.append(buf)
×
698
                return_buffers.append((buf, 1, None, None, primitive_type))
×
699
                converted_args.append(_ctypes_byref(buf))
×
700

701
            elif arg_type == _ARG_TYPE_SHAPE:
×
702
                converted_args.append(_c_int64(shape_symbol_values.get(info[2], 0)))
×
703

704
            elif arg_type == _ARG_TYPE_USER_ARRAY:
×
705
                arg = args[info[1]]
×
706
                shape_symbol_values[info[2]] = arg
×
707
                if _is_device_array(arg):
×
708
                    any_device_input = True
×
709
                arg = self._ensure_device_array(arg, keepalive, writebacks)
×
710
                converted_args.append(_ctypes_cast(_device_array_ptr(arg), info[3]))
×
711

712
            elif arg_type == _ARG_TYPE_USER_STRUCT:
×
713
                raise NotImplementedError(
×
714
                    "Structure arguments are not supported for device-resident "
715
                    "execution."
716
                )
717

718
            else:  # _ARG_TYPE_USER_SCALAR
719
                arg = args[info[1]]
×
720
                shape_symbol_values[info[2]] = arg
×
721
                converted_args.append(info[3](arg))
×
722

723
        # 3. Call the function
724
        func_result = self.func(*converted_args)
×
725

726
        # 3b. Mirror device results back into host inputs that were copied to the
727
        # device, so in-place writes (e.g. output arguments) are visible to the
728
        # caller. Read-only inputs are unchanged and copy back identically.
729
        for host_arg, device_arg in writebacks:
×
730
            host_view = cupy.asnumpy(device_arg)
×
731
            if isinstance(host_arg, np.ndarray):
×
732
                np.copyto(host_arg, host_view.reshape(host_arg.shape))
×
733
            elif hasattr(host_arg, "copy_"):  # torch CPU tensor
×
734
                import torch
×
735

736
                host_arg.copy_(
×
737
                    torch.from_numpy(host_view.reshape(tuple(host_arg.shape)))
738
                )
739

740
        # 4. Process returns using pre-sorted order
741
        if not return_buffers:
×
742
            return None
×
743

744
        # Host callers get host (numpy) outputs; device callers get cupy outputs.
745
        host_mode = not any_device_input
×
746

747
        num_outputs = len(return_buffers)
×
748
        results = [None] * num_outputs
×
749

750
        buf_idx = 0
×
751
        for i, info in enumerate(self._arg_info):
×
752
            arg_type = info[0]
×
753
            if arg_type not in (_ARG_TYPE_OUTPUT_ARRAY, _ARG_TYPE_OUTPUT_SCALAR):
×
754
                continue
×
755

756
            result_pos = self._output_pos_map[i]
×
757
            buf, size, dims, compiled_strides, primitive_type = return_buffers[buf_idx]
×
758
            buf_idx += 1
×
759

760
            if arg_type == _ARG_TYPE_OUTPUT_SCALAR:
×
761
                results[result_pos] = buf.value
×
762
            else:
763
                arr = buf
×
764
                if dims and len(dims) > 1:
×
765
                    arr = arr.reshape(dims)
×
766
                if host_mode:
×
767
                    arr = cupy.asnumpy(arr)
×
768
                results[result_pos] = arr
×
769

770
        if len(results) == 1:
×
771
            return results[0]
×
772
        return tuple(results) if results else None
×
773

774
    def _resolve_call_mode(self, args):
4✔
775
        """Classify the call's array arguments into exactly one call mode.
776

777
        All array arguments must be the same kind; mixing numpy / cupy / CPU
778
        torch / CUDA torch arrays in a single call is rejected. Calls without any
779
        array argument default to ``NumPyCPU`` (host execution).
780
        """
781
        modes = set()
4✔
782
        for info in self._arg_info:
4✔
783
            arg_type = info[0]
4✔
784
            if arg_type == _ARG_TYPE_USER_ARRAY or arg_type == _ARG_TYPE_USER_STRUCT:
4✔
785
                kind = _classify_array_kind(args[info[1]])
4✔
786
                if kind is not None:
4✔
787
                    modes.add(kind)
4✔
788

789
        if not modes:
4✔
790
            return _CALL_MODE_NUMPY_CPU
4✔
791
        if len(modes) > 1:
4✔
792
            raise TypeError(
×
793
                "Mixed array kinds are not allowed in a single call; every array "
794
                f"argument must be the same kind, but got {sorted(modes)}. "
795
                "Provide all inputs (and outputs) as one of: numpy arrays, cupy "
796
                "arrays, CPU torch tensors, or CUDA torch tensors."
797
            )
798
        return next(iter(modes))
4✔
799

800
    def __call__(self, *args):
4✔
801
        """Execute the compiled artifact, dispatching by call mode.
802

803
        Exactly one call mode is allowed per call (no mixing of array kinds).
804
        GPU modes (cupy / CUDA torch) require a device-resident artifact; host
805
        modes (numpy / CPU torch) run on the device with a one-time performance
806
        warning when the artifact is device-resident, otherwise on the host.
807
        """
808
        mode = self._resolve_call_mode(args)
4✔
809

810
        if mode in _GPU_CALL_MODES and not self.device_resident:
4✔
811
            raise TypeError(
×
812
                f"{mode} arguments were provided, but this artifact is not "
813
                "device-resident. GPU arrays can only be used with a "
814
                "device-resident artifact (a fully-offloadable kernel compiled "
815
                "for a GPU target). Provide host arrays (numpy arrays or CPU "
816
                "torch tensors) instead."
817
            )
818

819
        # Device-resident artifacts consume/produce device pointers directly.
820
        # Host inputs are copied to the device inside _call_device (with a
821
        # one-time warning); device inputs are consumed zero-copy.
822
        if self.device_resident:
4✔
823
            return self._call_device(*args)
×
824

825
        # Host execution path. CPU torch tensors are converted to numpy views so
826
        # the ctypes boundary can take their data pointer.
827
        # Inform once when a GPU target falls back to host execution because the
828
        # device-residency optimization did not apply to this artifact.
829
        if self.target in ("cuda", "rocm") and not self._warned_residency_failed:
4✔
830
            self._warned_residency_failed = True
×
831
            warn_device_residency_failed(self.target)
×
832

833
        if mode == _CALL_MODE_TORCH_CPU:
4✔
834
            args = tuple(
×
835
                a.detach().cpu().contiguous().numpy() if _is_torch_tensor(a) else a
836
                for a in args
837
            )
838
        return self._call_host(*args)
4✔
839

840
    def _call_host(self, *args):
4✔
841
        # Ultra-fast path using pre-computed tuple-based argument info
842
        # Local variable caching for speed
843
        _eval = eval
4✔
844
        _GLOBALS = _EVAL_GLOBALS
4✔
845
        _np_empty = np.empty
4✔
846

847
        # 1. Build shape_symbol_values from shape sources (pre-computed list)
848
        shape_symbol_values = {}
4✔
849
        for s_idx, u_idx, dim_idx, key in self._shape_sources_list:
4✔
850
            if u_idx < len(args):
4✔
851
                shape_symbol_values[key] = args[u_idx].shape[dim_idx]
4✔
852

853
        # 2. Process arguments using tuple-based dispatch
854
        converted_args = []
4✔
855
        structure_refs = (
4✔
856
            []
857
        )  # Keep refs alive (includes numpy arrays for output buffers)
858
        return_buffers = (
4✔
859
            []
860
        )  # List of (np_arr, size, dims, compiled_strides, primitive_type)
861
        # Structs whose scalar members must be copied back into the Python
862
        # object after the call: (python_obj, c_struct, sorted_members).
863
        struct_writebacks = []
4✔
864

865
        for info in self._arg_info:
4✔
866
            arg_type = info[0]
4✔
867

868
            if arg_type == _ARG_TYPE_OUTPUT_ARRAY:
4✔
869
                # info = (type, name, base_type, target_type, compiled_dims, compiled_strides, primitive_type, np_dtype)
870
                target_type = info[3]
4✔
871
                compiled_dims = info[4]
4✔
872
                compiled_strides = info[5]
4✔
873
                np_dtype = info[7]
4✔
874

875
                # Evaluate size from compiled code objects
876
                size = 1
4✔
877
                dims = []
4✔
878
                for code in compiled_dims:
4✔
879
                    d = int(_eval(code, _GLOBALS, shape_symbol_values))
4✔
880
                    dims.append(d)
4✔
881
                    size *= d
4✔
882

883
                # Use numpy for fast allocation (much faster than ctypes)
884
                buf_arr = _np_empty(size, dtype=np_dtype)
4✔
885
                structure_refs.append(buf_arr)  # Keep alive
4✔
886
                return_buffers.append((buf_arr, size, dims, compiled_strides, info[6]))
4✔
887
                # Get pointer directly from numpy array interface
888
                ptr = buf_arr.ctypes.data
4✔
889
                converted_args.append(_ctypes_cast(ptr, target_type))
4✔
890

891
            elif arg_type == _ARG_TYPE_OUTPUT_SCALAR:
4✔
892
                # info = (type, name, base_type, primitive_type)
893
                base_type = info[2]
4✔
894
                primitive_type = info[3]
4✔
895
                buf = base_type()
4✔
896
                structure_refs.append(buf)
4✔
897
                return_buffers.append((buf, 1, None, None, primitive_type))
4✔
898
                converted_args.append(_ctypes_byref(buf))
4✔
899

900
            elif arg_type == _ARG_TYPE_SHAPE:
4✔
901
                # info = (type, s_idx, key_str)
902
                converted_args.append(_c_int64(shape_symbol_values.get(info[2], 0)))
4✔
903

904
            elif arg_type == _ARG_TYPE_USER_ARRAY:
4✔
905
                # info = (type, user_idx, name, target_type)
906
                user_idx = info[1]
4✔
907
                arg = args[user_idx]
4✔
908
                shape_symbol_values[info[2]] = arg  # For indirect access
4✔
909
                # Direct pointer access - faster than data_as()
910
                converted_args.append(_ctypes_cast(arg.ctypes.data, info[3]))
4✔
911

912
            elif arg_type == _ARG_TYPE_USER_STRUCT:
4✔
913
                # info = (type, user_idx, name, struct_class, sorted_members)
914
                arg = args[info[1]]
4✔
915
                shape_symbol_values[info[2]] = arg
4✔
916
                struct_class = info[3]
4✔
917
                struct_values = {}
4✔
918
                for m in info[4]:
4✔
919
                    member_name = m[0]
4✔
920
                    if not hasattr(arg, member_name):
4✔
921
                        continue
×
922
                    member_value = getattr(arg, member_name)
4✔
923
                    member_type = m[1][1]
4✔
924
                    if isinstance(member_value, np.ndarray):
4✔
925
                        # Array member: pass the data pointer (struct-of-arrays).
926
                        struct_values[member_name] = member_value.ctypes.data_as(
4✔
927
                            self._get_ctypes_type(member_type)
928
                        )
929
                    else:
930
                        struct_values[member_name] = member_value
4✔
931
                c_struct = struct_class(**struct_values)
4✔
932
                structure_refs.append(c_struct)
4✔
933
                converted_args.append(_ctypes_pointer(c_struct))
4✔
934
                # Record for scalar-member write-back after the call.
935
                struct_writebacks.append((arg, c_struct, info[4]))
4✔
936

937
            else:  # _ARG_TYPE_USER_SCALAR
938
                # info = (type, user_idx, name, target_type)
939
                arg = args[info[1]]
4✔
940
                shape_symbol_values[info[2]] = arg
4✔
941
                converted_args.append(info[3](arg))
4✔
942

943
        # 3. Call the function
944
        func_result = self.func(*converted_args)
4✔
945

946
        # 3b. Copy scalar struct members modified in-place back into the Python
947
        # object. Array members share the numpy buffer via their data pointer,
948
        # but scalar members are passed by value and must be mirrored back so
949
        # writes like `domain.dtcourant = ...` are visible to the caller.
950
        for py_obj, c_struct, sorted_members in struct_writebacks:
4✔
951
            for m in sorted_members:
4✔
952
                member_name = m[0]
4✔
953
                if isinstance(m[1][1], Pointer):
4✔
954
                    continue  # array member: written in place via the pointer
4✔
955
                if hasattr(py_obj, member_name):
4✔
956
                    setattr(py_obj, member_name, getattr(c_struct, member_name))
4✔
957

958
        # 4. Process returns using pre-sorted order
959
        if not return_buffers:
4✔
960
            if func_result is not None:
4✔
961
                return self._convert_return_value(func_result, shape_symbol_values)
4✔
962
            return None
4✔
963

964
        # return_buffers: [(np_arr_or_ctypes_scalar, size, dims, compiled_strides, primitive_type), ...]
965
        num_outputs = len(return_buffers)
4✔
966
        results = [None] * num_outputs
4✔
967

968
        buf_idx = 0
4✔
969
        for i, info in enumerate(self._arg_info):
4✔
970
            arg_type = info[0]
4✔
971
            if arg_type not in (_ARG_TYPE_OUTPUT_ARRAY, _ARG_TYPE_OUTPUT_SCALAR):
4✔
972
                continue
4✔
973

974
            result_pos = self._output_pos_map[i]
4✔
975
            buf, size, dims, compiled_strides, primitive_type = return_buffers[buf_idx]
4✔
976
            buf_idx += 1
4✔
977

978
            if arg_type == _ARG_TYPE_OUTPUT_SCALAR:
4✔
979
                # Scalar - buf is a ctypes scalar
980
                results[result_pos] = buf.value
4✔
981
            else:
982
                # Array - buf is already a numpy array
983
                arr = buf
4✔
984
                if dims and len(dims) > 1:
4✔
985
                    # Need to reshape
986
                    if compiled_strides:
4✔
987
                        try:
4✔
988
                            itemsize = arr.itemsize
4✔
989
                            byte_strides = tuple(
4✔
990
                                int(_eval(s, {}, shape_symbol_values)) * itemsize
991
                                for s in compiled_strides
992
                            )
993
                            arr = np.lib.stride_tricks.as_strided(
4✔
994
                                arr, shape=dims, strides=byte_strides
995
                            )
996
                        except:
4✔
997
                            arr = arr.reshape(dims)
4✔
998
                    else:
999
                        arr = arr.reshape(dims)
×
1000
                elif dims and len(dims) == 1:
4✔
1001
                    pass  # Already 1D with correct size
4✔
1002
                results[result_pos] = arr
4✔
1003

1004
        if len(results) == 1:
4✔
1005
            return results[0]
4✔
1006
        return tuple(results) if results else None
4✔
1007

1008
    def get_return_shape(self, *args):
4✔
1009
        shape_str = self.sdfg.metadata("return_shape")
4✔
1010
        if not shape_str:
4✔
1011
            return None
4✔
1012

1013
        shape_exprs = shape_str.split(",")
×
1014

1015
        # Reconstruct shape values
1016
        shape_values = {}
×
1017
        for i, (arg_idx, dim_idx) in enumerate(self.shape_sources):
×
1018
            arg = args[arg_idx]
×
1019
            if np is not None and isinstance(arg, np.ndarray):
×
1020
                val = arg.shape[dim_idx]
×
1021
                shape_values[f"_s{i}"] = val
×
1022

1023
        # Add scalar arguments to shape_values
1024
        # We assume the first len(args) arguments in sdfg.arguments correspond to the user arguments
1025
        if hasattr(self.sdfg, "arguments"):
×
1026
            for arg_name, arg_val in zip(self.sdfg.arguments, args):
×
1027
                if isinstance(arg_val, (int, np.integer)):
×
1028
                    shape_values[arg_name] = int(arg_val)
×
1029

1030
        evaluated_shape = []
×
1031
        for expr in shape_exprs:
×
1032
            # Simple evaluation using eval with shape_values
1033
            # Warning: eval is unsafe, but here expressions come from our compiler
1034
            try:
×
1035
                val = eval(expr, _EVAL_GLOBALS, shape_values)
×
1036
                evaluated_shape.append(int(val))
×
1037
            except Exception:
×
1038
                return None
×
1039

1040
        return tuple(evaluated_shape)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc