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

daisytuner / docc / 28953512655

08 Jul 2026 03:11PM UTC coverage: 62.932% (+0.007%) from 62.925%
28953512655

push

github

web-flow
Merge pull request #848 from daisytuner/fix/nnParameters

fixed issue where tensors with grad causes runtime error

3 of 8 new or added lines in 1 file covered. (37.5%)

3 existing lines in 1 file now uncovered.

40601 of 64516 relevant lines covered (62.93%)

977.2 hits per line

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

61.95
/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)
×
NEW
60
    if getattr(arg, "__cuda_array_interface__", None) is not None:
×
NEW
61
        return True
×
UNCOV
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()
×
NEW
74
    cai = getattr(arg, "__cuda_array_interface__", None)
×
NEW
75
    if cai is not None:
×
NEW
76
        return cai["data"][0]
×
UNCOV
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✔
UNCOV
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
    ):
225
        self.lib_path = lib_path
4✔
226
        self.sdfg = sdfg
4✔
227
        self.shape_sources = shape_sources or []
4✔
228
        self.structure_member_info = structure_member_info or {}
4✔
229
        self.lib = ctypes.CDLL(lib_path)
4✔
230
        self.func = getattr(self.lib, sdfg.name)
4✔
231

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

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

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

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

260
        # Cache for ctypes structure definitions
261
        self._ctypes_structures = {}
4✔
262

263
        # Set up argument types
264
        self.arg_names = sdfg.arguments
4✔
265
        self.arg_types = []
4✔
266
        self.arg_sdfg_types = []  # Keep track of original sdfg types
4✔
267
        for arg_name in sdfg.arguments:
4✔
268
            arg_type = sdfg.type(arg_name)
4✔
269
            self.arg_sdfg_types.append(arg_type)
4✔
270
            ct_type = self._get_ctypes_type(arg_type)
4✔
271
            self.arg_types.append(ct_type)
4✔
272

273
        self.func.argtypes = self.arg_types
4✔
274

275
        # Set up return type
276
        self.func.restype = self._get_ctypes_type(sdfg.return_type)
4✔
277

278
        # Pre-compute argument classification for fast __call__
279
        self._precompute_arg_metadata()
4✔
280

281
    def _precompute_arg_metadata(self):
4✔
282
        """Pre-compute argument metadata for fast __call__ dispatch."""
283
        output_args_set = set(self.output_args)
4✔
284

285
        # Build shape source lookup: s_idx -> (u_idx, dim_idx)
286
        # Also pre-compute the shape keys
287
        self._shape_sources_list = []  # [(s_idx, u_idx, dim_idx, key_str), ...]
4✔
288
        for i, (u_idx, dim_idx) in enumerate(self.shape_sources):
4✔
289
            self._shape_sources_list.append((i, u_idx, dim_idx, f"_s{i}"))
4✔
290

291
        # Classify each argument using tuple-based info for faster access
292
        # Each entry is (arg_type, *type_specific_data)
293
        self._arg_info = []
4✔
294
        user_arg_counter = 0
4✔
295

296
        # For output ordering (avoid sorting at runtime)
297
        output_order = []
4✔
298

299
        for i, arg_name in enumerate(self.arg_names):
4✔
300
            if arg_name in output_args_set:
4✔
301
                # Output argument
302
                target_type = self.arg_types[i]
4✔
303
                base_type = target_type._type_
4✔
304
                sdfg_type = self.arg_sdfg_types[i]
4✔
305

306
                # Pre-compute primitive type for return processing
307
                primitive_type = None
4✔
308
                if isinstance(sdfg_type, Pointer) and sdfg_type.has_pointee_type():
4✔
309
                    pointee = sdfg_type.pointee_type
4✔
310
                    if isinstance(pointee, Scalar):
4✔
311
                        primitive_type = pointee.primitive_type
4✔
312

313
                if arg_name in self.output_shapes:
4✔
314
                    dims = self.output_shapes[arg_name]
4✔
315
                    # Always compile shape expressions - they may depend on runtime values
316
                    compiled_dims = []
4✔
317
                    for d in dims:
4✔
318
                        d_str = str(d)
4✔
319
                        expr = self._convert_to_python_syntax(d_str)
4✔
320
                        compiled_dims.append(compile(expr, "<shape>", "eval"))
4✔
321

322
                    # Pre-compile stride expressions if available
323
                    compiled_strides = None
4✔
324
                    if arg_name in self.output_strides:
4✔
325
                        compiled_strides = []
4✔
326
                        for s in self.output_strides[arg_name]:
4✔
327
                            expr = self._convert_to_python_syntax(str(s))
4✔
328
                            compiled_strides.append(compile(expr, "<stride>", "eval"))
4✔
329

330
                    # Get numpy dtype for fast allocation
331
                    np_dtype = (
4✔
332
                        _PRIMITIVE_TO_NP_DTYPE.get(primitive_type, np.float64)
333
                        if primitive_type
334
                        else np.float64
335
                    )
336

337
                    # Tuple: (arg_type, name, base_type, target_type, compiled_dims, compiled_strides, primitive_type, np_dtype)
338
                    info_idx = len(self._arg_info)
4✔
339
                    self._arg_info.append(
4✔
340
                        (
341
                            _ARG_TYPE_OUTPUT_ARRAY,
342
                            arg_name,
343
                            base_type,
344
                            target_type,
345
                            compiled_dims,
346
                            compiled_strides,
347
                            primitive_type,
348
                            np_dtype,
349
                        )
350
                    )
351
                    output_order.append((int(arg_name.split("_")[-1]), info_idx))
4✔
352
                else:
353
                    # Scalar return
354
                    info_idx = len(self._arg_info)
4✔
355
                    self._arg_info.append(
4✔
356
                        (_ARG_TYPE_OUTPUT_SCALAR, arg_name, base_type, primitive_type)
357
                    )
358
                    output_order.append((int(arg_name.split("_")[-1]), info_idx))
4✔
359

360
            elif arg_name.startswith("_s") and arg_name[2:].isdigit():
4✔
361
                # Shape symbol argument - tuple: (arg_type, s_idx, key_str)
362
                s_idx = int(arg_name[2:])
4✔
363
                self._arg_info.append((_ARG_TYPE_SHAPE, s_idx, f"_s{s_idx}"))
4✔
364
            else:
365
                # User argument
366
                sdfg_type = self.arg_sdfg_types[i]
4✔
367
                target_type = self.arg_types[i]
4✔
368
                is_struct_ptr = (
4✔
369
                    sdfg_type
370
                    and isinstance(sdfg_type, Pointer)
371
                    and sdfg_type.has_pointee_type()
372
                    and isinstance(sdfg_type.pointee_type, Structure)
373
                )
374

375
                if is_struct_ptr:
4✔
376
                    struct_name = sdfg_type.pointee_type.name
4✔
377
                    struct_class = self._create_ctypes_structure(struct_name)
4✔
378
                    members = self.structure_member_info[struct_name]
4✔
379
                    sorted_members = tuple(
4✔
380
                        sorted(members.items(), key=lambda x: x[1][0])
381
                    )
382
                    # Tuple: (arg_type, user_idx, name, struct_class, sorted_members)
383
                    self._arg_info.append(
4✔
384
                        (
385
                            _ARG_TYPE_USER_STRUCT,
386
                            user_arg_counter,
387
                            arg_name,
388
                            struct_class,
389
                            sorted_members,
390
                        )
391
                    )
392
                elif hasattr(target_type, "contents"):
4✔
393
                    # Array user arg - tuple: (arg_type, user_idx, name, target_type)
394
                    self._arg_info.append(
4✔
395
                        (_ARG_TYPE_USER_ARRAY, user_arg_counter, arg_name, target_type)
396
                    )
397
                else:
398
                    # Scalar user arg - tuple: (arg_type, user_idx, name, target_type)
399
                    self._arg_info.append(
4✔
400
                        (_ARG_TYPE_USER_SCALAR, user_arg_counter, arg_name, target_type)
401
                    )
402
                user_arg_counter += 1
4✔
403

404
        self._num_user_args = user_arg_counter
4✔
405

406
        # Pre-sort output order and build position map
407
        output_order.sort(key=lambda x: x[0])
4✔
408
        self._output_order = tuple(idx for _, idx in output_order)
4✔
409
        # Map from _arg_info index to result position (for O(1) lookup)
410
        self._output_pos_map = {idx: pos for pos, idx in enumerate(self._output_order)}
4✔
411

412
    def _convert_to_python_syntax(self, expr_str):
4✔
413
        result = expr_str
4✔
414

415
        while True:
4✔
416
            match = _FUNC_CALL_PATTERN.search(result)
4✔
417
            if not match:
4✔
418
                break
4✔
419

420
            name = match.group(1)
4✔
421
            index = match.group(2)
4✔
422

423
            if name.lower() in _KNOWN_FUNCTIONS:
4✔
424
                # Use unique delimiters that won't appear in expressions
425
                placeholder = f"@@@FUNC@@@{name}@@@{index}@@@END@@@"
4✔
426
                result = result[: match.start()] + placeholder + result[match.end() :]
4✔
427
            else:
428
                result = (
×
429
                    result[: match.start()] + f"{name}[{index}]" + result[match.end() :]
430
                )
431

432
        result = _PLACEHOLDER_PATTERN.sub(r"\1(\2)", result)
4✔
433

434
        return result
4✔
435

436
    def _create_ctypes_structure(self, struct_name):
4✔
437
        """Create a ctypes Structure class for the given structure name."""
438
        if struct_name in self._ctypes_structures:
4✔
439
            return self._ctypes_structures[struct_name]
4✔
440

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

444
        # Get member info: {member_name: (index, type)}
445
        members = self.structure_member_info[struct_name]
4✔
446
        # Sort by index to get correct order
447
        sorted_members = sorted(members.items(), key=lambda x: x[1][0])
4✔
448

449
        # Build _fields_ for ctypes.Structure
450
        fields = []
4✔
451
        for member_name, member_info in sorted_members:
4✔
452
            member_type = member_info[1]
4✔
453
            ct_type = self._get_ctypes_type(member_type)
4✔
454
            fields.append((member_name, ct_type))
4✔
455

456
        # Create the ctypes Structure class dynamically
457
        class CStructure(ctypes.Structure):
4✔
458
            _fields_ = fields
4✔
459

460
        self._ctypes_structures[struct_name] = CStructure
4✔
461
        return CStructure
4✔
462

463
    def _get_ctypes_type(self, sdfg_type):
4✔
464
        if isinstance(sdfg_type, Scalar):
4✔
465
            return _CTYPES_MAP.get(sdfg_type.primitive_type, ctypes.c_void_p)
4✔
466
        elif isinstance(sdfg_type, Array):
4✔
467
            # Arrays are passed as pointers
468
            elem_type = _CTYPES_MAP.get(sdfg_type.primitive_type, ctypes.c_void_p)
×
469
            return ctypes.POINTER(elem_type)
×
470
        elif isinstance(sdfg_type, Pointer):
4✔
471
            # Check if pointee is a Structure
472
            # Note: has_pointee_type() is guaranteed to exist on Pointer instances from C++ bindings
473
            if sdfg_type.has_pointee_type():
4✔
474
                pointee = sdfg_type.pointee_type
4✔
475
                if isinstance(pointee, Structure):
4✔
476
                    # Create ctypes structure and return pointer to it
477
                    struct_class = self._create_ctypes_structure(pointee.name)
4✔
478
                    return ctypes.POINTER(struct_class)
4✔
479
                elif isinstance(pointee, Scalar):
4✔
480
                    elem_type = _CTYPES_MAP.get(pointee.primitive_type, ctypes.c_void_p)
4✔
481
                    return ctypes.POINTER(elem_type)
4✔
482
            return ctypes.c_void_p
×
483
        return ctypes.c_void_p
×
484

485
    def _convert_return_value(self, func_result, shape_symbol_values):
4✔
486
        return_type = self.sdfg.return_type
4✔
487

488
        if isinstance(return_type, Pointer):
4✔
489
            if return_type.has_pointee_type():
×
490
                pointee = return_type.pointee_type
×
491
                if isinstance(pointee, Scalar):
×
492
                    # Pointer to scalar element type - need to determine array size
493
                    # Get return shape from metadata if available
494
                    return_shape_str = self.sdfg.metadata("return_shape")
×
495
                    if return_shape_str:
×
496
                        # Strip brackets (metadata may be "[10,10]" format)
497
                        return_shape_str = return_shape_str.strip("[]")
×
498
                        shape = []
×
499
                        for dim_str in return_shape_str.split(","):
×
500
                            try:
×
501
                                eval_str = self._convert_to_python_syntax(str(dim_str))
×
502
                                val = eval(eval_str, _EVAL_GLOBALS, shape_symbol_values)
×
503
                                shape.append(int(val))
×
504
                            except Exception:
×
505
                                # Can't evaluate shape, return raw pointer
506
                                return func_result
×
507

508
                        # Determine numpy dtype from primitive type
509
                        dtype = _NUMPY_DTYPE_MAP.get(pointee.primitive_type, np.float64)
×
510

511
                        # Calculate total size
512
                        total_size = 1
×
513
                        for dim in shape:
×
514
                            total_size *= dim
×
515

516
                        # Create numpy array from pointer
517
                        ct_type = _CTYPES_MAP.get(
×
518
                            pointee.primitive_type, ctypes.c_double
519
                        )
520
                        arr_type = ct_type * total_size
×
521
                        # For Half/BFloat, use np.frombuffer since np.ctypeslib.as_array
522
                        # doesn't support these types (PEP 3118 buffer format limitation)
523
                        if pointee.primitive_type in (
×
524
                            PrimitiveType.Half,
525
                            PrimitiveType.BFloat,
526
                        ):
527
                            byte_size = total_size * 2  # Half and BFloat are 2 bytes
×
528
                            arr = np.frombuffer(
×
529
                                (ctypes.c_char * byte_size).from_address(
530
                                    ctypes.cast(func_result, ctypes.c_void_p).value
531
                                ),
532
                                dtype=dtype,
533
                            ).copy()
534
                        else:
535
                            arr = np.ctypeslib.as_array(
×
536
                                ctypes.cast(
537
                                    func_result, ctypes.POINTER(arr_type)
538
                                ).contents
539
                            )
540
                        return arr.reshape(shape)
×
541
                    else:
542
                        # No shape info - try to infer from input shapes
543
                        # For identity-like operations, the output shape matches input
544
                        if len(self.shape_sources) > 0 and len(shape_symbol_values) > 0:
×
545
                            # Use first input's shape as a fallback
546
                            shape = []
×
547
                            for i in range(len(self.shape_sources)):
×
548
                                if f"_s{i}" in shape_symbol_values:
×
549
                                    shape.append(shape_symbol_values[f"_s{i}"])
×
550

551
                            if shape:
×
552
                                dtype = _NUMPY_DTYPE_MAP.get(
×
553
                                    pointee.primitive_type, np.float64
554
                                )
555

556
                                total_size = 1
×
557
                                for dim in shape:
×
558
                                    total_size *= dim
×
559

560
                                ct_type = _CTYPES_MAP.get(
×
561
                                    pointee.primitive_type, ctypes.c_double
562
                                )
563
                                arr_type = ct_type * total_size
×
564
                                # For Half/BFloat, use np.frombuffer since np.ctypeslib.as_array
565
                                # doesn't support these types (PEP 3118 buffer format limitation)
566
                                if pointee.primitive_type in (
×
567
                                    PrimitiveType.Half,
568
                                    PrimitiveType.BFloat,
569
                                ):
570
                                    byte_size = (
×
571
                                        total_size * 2
572
                                    )  # Half and BFloat are 2 bytes
573
                                    arr = np.frombuffer(
×
574
                                        (ctypes.c_char * byte_size).from_address(
575
                                            ctypes.cast(
576
                                                func_result, ctypes.c_void_p
577
                                            ).value
578
                                        ),
579
                                        dtype=dtype,
580
                                    ).copy()
581
                                else:
582
                                    arr = np.ctypeslib.as_array(
×
583
                                        ctypes.cast(
584
                                            func_result, ctypes.POINTER(arr_type)
585
                                        ).contents
586
                                    )
587
                                return arr.reshape(shape)
×
588

589
                        # Can't determine shape, return raw pointer
590
                        return func_result
×
591
        elif isinstance(return_type, Scalar):
4✔
592
            return func_result
4✔
593

594
        return func_result
×
595

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

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

604
        The copied device array is appended to ``keepalive`` so it outlives the
605
        call, and the ``(host, device)`` pair is recorded in ``writebacks`` so
606
        that in-place writes (e.g. output arguments) are mirrored back to the
607
        original host array after execution.
608
        """
609
        if _is_device_array(arg):
×
610
            return arg
×
611

612
        import cupy
×
613

614
        host = arg
×
615
        # Convert a CPU torch tensor to numpy first; cupy.asarray handles numpy.
616
        if hasattr(host, "detach") and hasattr(host, "numpy"):
×
617
            host = host.detach().numpy()
×
618
        device_arg = cupy.asarray(host)
×
619
        keepalive.append(device_arg)
×
620
        writebacks.append((arg, device_arg))
×
621

622
        if not self._warned_host_to_device:
×
623
            self._warned_host_to_device = True
×
624
            warn_host_to_device_copies(self.device_backend or "cuda")
×
625

626
        return device_arg
×
627

628
    def _call_device(self, *args):
4✔
629
        """Execute with device-resident arguments (cupy / torch.cuda).
630

631
        Inputs are passed as raw device pointers and output arrays are allocated
632
        on the device, so no host<->device copies happen at the call boundary.
633
        Outputs are returned as cupy arrays (zero-copy interoperable with torch
634
        via DLPack / __cuda_array_interface__).
635
        """
636
        import cupy
×
637

638
        _eval = eval
×
639
        _GLOBALS = _EVAL_GLOBALS
×
640
        # 1. Build shape_symbol_values from shape sources
641
        shape_symbol_values = {}
×
642
        for s_idx, u_idx, dim_idx, key in self._shape_sources_list:
×
643
            if u_idx < len(args):
×
644
                shape_symbol_values[key] = args[u_idx].shape[dim_idx]
×
645

646
        # 2. Process arguments
647
        converted_args = []
×
648
        keepalive = []  # keep device buffers / ctypes scalars alive
×
649
        writebacks = []  # (host_arg, device_arg) for host inputs copied to device
×
650
        return_buffers = []  # (buf, size, dims, compiled_strides, primitive_type)
×
651
        # Track whether the caller supplied device arrays. When all array inputs
652
        # are host arrays (numpy / CPU torch), the caller works in host space and
653
        # expects host outputs, so device output buffers are copied back to host.
654
        any_device_input = False
×
655

656
        for info in self._arg_info:
×
657
            arg_type = info[0]
×
658

659
            if arg_type == _ARG_TYPE_OUTPUT_ARRAY:
×
660
                target_type = info[3]
×
661
                compiled_dims = info[4]
×
662
                compiled_strides = info[5]
×
663
                np_dtype = info[7]
×
664

665
                size = 1
×
666
                dims = []
×
667
                for code in compiled_dims:
×
668
                    d = int(_eval(code, _GLOBALS, shape_symbol_values))
×
669
                    dims.append(d)
×
670
                    size *= d
×
671

672
                buf = cupy.empty(size, dtype=np_dtype)
×
673
                keepalive.append(buf)
×
674
                return_buffers.append((buf, size, dims, compiled_strides, info[6]))
×
675
                converted_args.append(_ctypes_cast(int(buf.data.ptr), target_type))
×
676

677
            elif arg_type == _ARG_TYPE_OUTPUT_SCALAR:
×
678
                base_type = info[2]
×
679
                primitive_type = info[3]
×
680
                buf = base_type()
×
681
                keepalive.append(buf)
×
682
                return_buffers.append((buf, 1, None, None, primitive_type))
×
683
                converted_args.append(_ctypes_byref(buf))
×
684

685
            elif arg_type == _ARG_TYPE_SHAPE:
×
686
                converted_args.append(_c_int64(shape_symbol_values.get(info[2], 0)))
×
687

688
            elif arg_type == _ARG_TYPE_USER_ARRAY:
×
689
                arg = args[info[1]]
×
690
                shape_symbol_values[info[2]] = arg
×
691
                if _is_device_array(arg):
×
692
                    any_device_input = True
×
693
                arg = self._ensure_device_array(arg, keepalive, writebacks)
×
694
                converted_args.append(_ctypes_cast(_device_array_ptr(arg), info[3]))
×
695

696
            elif arg_type == _ARG_TYPE_USER_STRUCT:
×
697
                raise NotImplementedError(
×
698
                    "Structure arguments are not supported for device-resident "
699
                    "execution."
700
                )
701

702
            else:  # _ARG_TYPE_USER_SCALAR
703
                arg = args[info[1]]
×
704
                shape_symbol_values[info[2]] = arg
×
705
                converted_args.append(info[3](arg))
×
706

707
        # 3. Call the function
708
        func_result = self.func(*converted_args)
×
709

710
        # 3b. Mirror device results back into host inputs that were copied to the
711
        # device, so in-place writes (e.g. output arguments) are visible to the
712
        # caller. Read-only inputs are unchanged and copy back identically.
713
        for host_arg, device_arg in writebacks:
×
714
            host_view = cupy.asnumpy(device_arg)
×
715
            if isinstance(host_arg, np.ndarray):
×
716
                np.copyto(host_arg, host_view.reshape(host_arg.shape))
×
717
            elif hasattr(host_arg, "copy_"):  # torch CPU tensor
×
718
                import torch
×
719

720
                host_arg.copy_(
×
721
                    torch.from_numpy(host_view.reshape(tuple(host_arg.shape)))
722
                )
723

724
        # 4. Process returns using pre-sorted order
725
        if not return_buffers:
×
726
            return None
×
727

728
        # Host callers get host (numpy) outputs; device callers get cupy outputs.
729
        host_mode = not any_device_input
×
730

731
        num_outputs = len(return_buffers)
×
732
        results = [None] * num_outputs
×
733

734
        buf_idx = 0
×
735
        for i, info in enumerate(self._arg_info):
×
736
            arg_type = info[0]
×
737
            if arg_type not in (_ARG_TYPE_OUTPUT_ARRAY, _ARG_TYPE_OUTPUT_SCALAR):
×
738
                continue
×
739

740
            result_pos = self._output_pos_map[i]
×
741
            buf, size, dims, compiled_strides, primitive_type = return_buffers[buf_idx]
×
742
            buf_idx += 1
×
743

744
            if arg_type == _ARG_TYPE_OUTPUT_SCALAR:
×
745
                results[result_pos] = buf.value
×
746
            else:
747
                arr = buf
×
748
                if dims and len(dims) > 1:
×
749
                    arr = arr.reshape(dims)
×
750
                if host_mode:
×
751
                    arr = cupy.asnumpy(arr)
×
752
                results[result_pos] = arr
×
753

754
        if len(results) == 1:
×
755
            return results[0]
×
756
        return tuple(results) if results else None
×
757

758
    def _resolve_call_mode(self, args):
4✔
759
        """Classify the call's array arguments into exactly one call mode.
760

761
        All array arguments must be the same kind; mixing numpy / cupy / CPU
762
        torch / CUDA torch arrays in a single call is rejected. Calls without any
763
        array argument default to ``NumPyCPU`` (host execution).
764
        """
765
        modes = set()
4✔
766
        for info in self._arg_info:
4✔
767
            arg_type = info[0]
4✔
768
            if arg_type == _ARG_TYPE_USER_ARRAY or arg_type == _ARG_TYPE_USER_STRUCT:
4✔
769
                kind = _classify_array_kind(args[info[1]])
4✔
770
                if kind is not None:
4✔
771
                    modes.add(kind)
4✔
772

773
        if not modes:
4✔
774
            return _CALL_MODE_NUMPY_CPU
4✔
775
        if len(modes) > 1:
4✔
776
            raise TypeError(
×
777
                "Mixed array kinds are not allowed in a single call; every array "
778
                f"argument must be the same kind, but got {sorted(modes)}. "
779
                "Provide all inputs (and outputs) as one of: numpy arrays, cupy "
780
                "arrays, CPU torch tensors, or CUDA torch tensors."
781
            )
782
        return next(iter(modes))
4✔
783

784
    def __call__(self, *args):
4✔
785
        """Execute the compiled artifact, dispatching by call mode.
786

787
        Exactly one call mode is allowed per call (no mixing of array kinds).
788
        GPU modes (cupy / CUDA torch) require a device-resident artifact; host
789
        modes (numpy / CPU torch) run on the device with a one-time performance
790
        warning when the artifact is device-resident, otherwise on the host.
791
        """
792
        mode = self._resolve_call_mode(args)
4✔
793

794
        if mode in _GPU_CALL_MODES and not self.device_resident:
4✔
795
            raise TypeError(
×
796
                f"{mode} arguments were provided, but this artifact is not "
797
                "device-resident. GPU arrays can only be used with a "
798
                "device-resident artifact (a fully-offloadable kernel compiled "
799
                "for a GPU target). Provide host arrays (numpy arrays or CPU "
800
                "torch tensors) instead."
801
            )
802

803
        # Device-resident artifacts consume/produce device pointers directly.
804
        # Host inputs are copied to the device inside _call_device (with a
805
        # one-time warning); device inputs are consumed zero-copy.
806
        if self.device_resident:
4✔
807
            return self._call_device(*args)
×
808

809
        # Host execution path. CPU torch tensors are converted to numpy views so
810
        # the ctypes boundary can take their data pointer.
811
        # Inform once when a GPU target falls back to host execution because the
812
        # device-residency optimization did not apply to this artifact.
813
        if self.target in ("cuda", "rocm") and not self._warned_residency_failed:
4✔
814
            self._warned_residency_failed = True
×
815
            warn_device_residency_failed(self.target)
×
816

817
        if mode == _CALL_MODE_TORCH_CPU:
4✔
818
            args = tuple(
×
819
                a.detach().cpu().contiguous().numpy() if _is_torch_tensor(a) else a
820
                for a in args
821
            )
822
        return self._call_host(*args)
4✔
823

824
    def _call_host(self, *args):
4✔
825
        # Ultra-fast path using pre-computed tuple-based argument info
826
        # Local variable caching for speed
827
        _eval = eval
4✔
828
        _GLOBALS = _EVAL_GLOBALS
4✔
829
        _np_empty = np.empty
4✔
830

831
        # 1. Build shape_symbol_values from shape sources (pre-computed list)
832
        shape_symbol_values = {}
4✔
833
        for s_idx, u_idx, dim_idx, key in self._shape_sources_list:
4✔
834
            if u_idx < len(args):
4✔
835
                shape_symbol_values[key] = args[u_idx].shape[dim_idx]
4✔
836

837
        # 2. Process arguments using tuple-based dispatch
838
        converted_args = []
4✔
839
        structure_refs = (
4✔
840
            []
841
        )  # Keep refs alive (includes numpy arrays for output buffers)
842
        return_buffers = (
4✔
843
            []
844
        )  # List of (np_arr, size, dims, compiled_strides, primitive_type)
845
        # Structs whose scalar members must be copied back into the Python
846
        # object after the call: (python_obj, c_struct, sorted_members).
847
        struct_writebacks = []
4✔
848

849
        for info in self._arg_info:
4✔
850
            arg_type = info[0]
4✔
851

852
            if arg_type == _ARG_TYPE_OUTPUT_ARRAY:
4✔
853
                # info = (type, name, base_type, target_type, compiled_dims, compiled_strides, primitive_type, np_dtype)
854
                target_type = info[3]
4✔
855
                compiled_dims = info[4]
4✔
856
                compiled_strides = info[5]
4✔
857
                np_dtype = info[7]
4✔
858

859
                # Evaluate size from compiled code objects
860
                size = 1
4✔
861
                dims = []
4✔
862
                for code in compiled_dims:
4✔
863
                    d = int(_eval(code, _GLOBALS, shape_symbol_values))
4✔
864
                    dims.append(d)
4✔
865
                    size *= d
4✔
866

867
                # Use numpy for fast allocation (much faster than ctypes)
868
                buf_arr = _np_empty(size, dtype=np_dtype)
4✔
869
                structure_refs.append(buf_arr)  # Keep alive
4✔
870
                return_buffers.append((buf_arr, size, dims, compiled_strides, info[6]))
4✔
871
                # Get pointer directly from numpy array interface
872
                ptr = buf_arr.ctypes.data
4✔
873
                converted_args.append(_ctypes_cast(ptr, target_type))
4✔
874

875
            elif arg_type == _ARG_TYPE_OUTPUT_SCALAR:
4✔
876
                # info = (type, name, base_type, primitive_type)
877
                base_type = info[2]
4✔
878
                primitive_type = info[3]
4✔
879
                buf = base_type()
4✔
880
                structure_refs.append(buf)
4✔
881
                return_buffers.append((buf, 1, None, None, primitive_type))
4✔
882
                converted_args.append(_ctypes_byref(buf))
4✔
883

884
            elif arg_type == _ARG_TYPE_SHAPE:
4✔
885
                # info = (type, s_idx, key_str)
886
                converted_args.append(_c_int64(shape_symbol_values.get(info[2], 0)))
4✔
887

888
            elif arg_type == _ARG_TYPE_USER_ARRAY:
4✔
889
                # info = (type, user_idx, name, target_type)
890
                user_idx = info[1]
4✔
891
                arg = args[user_idx]
4✔
892
                shape_symbol_values[info[2]] = arg  # For indirect access
4✔
893
                # Direct pointer access - faster than data_as()
894
                converted_args.append(_ctypes_cast(arg.ctypes.data, info[3]))
4✔
895

896
            elif arg_type == _ARG_TYPE_USER_STRUCT:
4✔
897
                # info = (type, user_idx, name, struct_class, sorted_members)
898
                arg = args[info[1]]
4✔
899
                shape_symbol_values[info[2]] = arg
4✔
900
                struct_class = info[3]
4✔
901
                struct_values = {}
4✔
902
                for m in info[4]:
4✔
903
                    member_name = m[0]
4✔
904
                    if not hasattr(arg, member_name):
4✔
905
                        continue
×
906
                    member_value = getattr(arg, member_name)
4✔
907
                    member_type = m[1][1]
4✔
908
                    if isinstance(member_value, np.ndarray):
4✔
909
                        # Array member: pass the data pointer (struct-of-arrays).
910
                        struct_values[member_name] = member_value.ctypes.data_as(
4✔
911
                            self._get_ctypes_type(member_type)
912
                        )
913
                    else:
914
                        struct_values[member_name] = member_value
4✔
915
                c_struct = struct_class(**struct_values)
4✔
916
                structure_refs.append(c_struct)
4✔
917
                converted_args.append(_ctypes_pointer(c_struct))
4✔
918
                # Record for scalar-member write-back after the call.
919
                struct_writebacks.append((arg, c_struct, info[4]))
4✔
920

921
            else:  # _ARG_TYPE_USER_SCALAR
922
                # info = (type, user_idx, name, target_type)
923
                arg = args[info[1]]
4✔
924
                shape_symbol_values[info[2]] = arg
4✔
925
                converted_args.append(info[3](arg))
4✔
926

927
        # 3. Call the function
928
        func_result = self.func(*converted_args)
4✔
929

930
        # 3b. Copy scalar struct members modified in-place back into the Python
931
        # object. Array members share the numpy buffer via their data pointer,
932
        # but scalar members are passed by value and must be mirrored back so
933
        # writes like `domain.dtcourant = ...` are visible to the caller.
934
        for py_obj, c_struct, sorted_members in struct_writebacks:
4✔
935
            for m in sorted_members:
4✔
936
                member_name = m[0]
4✔
937
                if isinstance(m[1][1], Pointer):
4✔
938
                    continue  # array member: written in place via the pointer
4✔
939
                if hasattr(py_obj, member_name):
4✔
940
                    setattr(py_obj, member_name, getattr(c_struct, member_name))
4✔
941

942
        # 4. Process returns using pre-sorted order
943
        if not return_buffers:
4✔
944
            if func_result is not None:
4✔
945
                return self._convert_return_value(func_result, shape_symbol_values)
4✔
946
            return None
4✔
947

948
        # return_buffers: [(np_arr_or_ctypes_scalar, size, dims, compiled_strides, primitive_type), ...]
949
        num_outputs = len(return_buffers)
4✔
950
        results = [None] * num_outputs
4✔
951

952
        buf_idx = 0
4✔
953
        for i, info in enumerate(self._arg_info):
4✔
954
            arg_type = info[0]
4✔
955
            if arg_type not in (_ARG_TYPE_OUTPUT_ARRAY, _ARG_TYPE_OUTPUT_SCALAR):
4✔
956
                continue
4✔
957

958
            result_pos = self._output_pos_map[i]
4✔
959
            buf, size, dims, compiled_strides, primitive_type = return_buffers[buf_idx]
4✔
960
            buf_idx += 1
4✔
961

962
            if arg_type == _ARG_TYPE_OUTPUT_SCALAR:
4✔
963
                # Scalar - buf is a ctypes scalar
964
                results[result_pos] = buf.value
4✔
965
            else:
966
                # Array - buf is already a numpy array
967
                arr = buf
4✔
968
                if dims and len(dims) > 1:
4✔
969
                    # Need to reshape
970
                    if compiled_strides:
4✔
971
                        try:
4✔
972
                            itemsize = arr.itemsize
4✔
973
                            byte_strides = tuple(
4✔
974
                                int(_eval(s, {}, shape_symbol_values)) * itemsize
975
                                for s in compiled_strides
976
                            )
977
                            arr = np.lib.stride_tricks.as_strided(
4✔
978
                                arr, shape=dims, strides=byte_strides
979
                            )
980
                        except:
4✔
981
                            arr = arr.reshape(dims)
4✔
982
                    else:
983
                        arr = arr.reshape(dims)
×
984
                elif dims and len(dims) == 1:
4✔
985
                    pass  # Already 1D with correct size
4✔
986
                results[result_pos] = arr
4✔
987

988
        if len(results) == 1:
4✔
989
            return results[0]
4✔
990
        return tuple(results) if results else None
4✔
991

992
    def get_return_shape(self, *args):
4✔
993
        shape_str = self.sdfg.metadata("return_shape")
4✔
994
        if not shape_str:
4✔
995
            return None
4✔
996

997
        shape_exprs = shape_str.split(",")
×
998

999
        # Reconstruct shape values
1000
        shape_values = {}
×
1001
        for i, (arg_idx, dim_idx) in enumerate(self.shape_sources):
×
1002
            arg = args[arg_idx]
×
1003
            if np is not None and isinstance(arg, np.ndarray):
×
1004
                val = arg.shape[dim_idx]
×
1005
                shape_values[f"_s{i}"] = val
×
1006

1007
        # Add scalar arguments to shape_values
1008
        # We assume the first len(args) arguments in sdfg.arguments correspond to the user arguments
1009
        if hasattr(self.sdfg, "arguments"):
×
1010
            for arg_name, arg_val in zip(self.sdfg.arguments, args):
×
1011
                if isinstance(arg_val, (int, np.integer)):
×
1012
                    shape_values[arg_name] = int(arg_val)
×
1013

1014
        evaluated_shape = []
×
1015
        for expr in shape_exprs:
×
1016
            # Simple evaluation using eval with shape_values
1017
            # Warning: eval is unsafe, but here expressions come from our compiler
1018
            try:
×
1019
                val = eval(expr, _EVAL_GLOBALS, shape_values)
×
1020
                evaluated_shape.append(int(val))
×
1021
            except Exception:
×
1022
                return None
×
1023

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

© 2026 Coveralls, Inc