Description
When using Numba's types.void (e.g., as a return type for a device function or in signatures), Numba's datamodel translates it to an LLVM i8* (opaque pointer ptr in LLVM 15+) instead of the actual LLVM void type (ir.VoidType()).
Note: This issue originates in the core numba package itself (specifically in numba.core.datamodel where types.void is registered to OpaqueModel), but it has a severe impact on numba-cuda when linking device code.
This causes the generated LLVM IR to treat void as a pointer type. In the Numba calling convention, this results in device functions taking an i8** return pointer argument rather than simply returning void. While this might accidentally work at runtime on some architectures, it breaks strict ABI requirements. More importantly, it causes fatal errors during Link Time Optimization (LTO) when linking Numba-generated IR with C/C++ device code that correctly declares the function as returning void.
Reproducer
import numba
from numba import types, cuda
from numba.core import datamodel
from numba.cuda.descriptor import cuda_target
# 1. Numba's default void type is translated to i8* via OpaqueModel
void_type = types.void
default_model = datamodel.default_manager.lookup(void_type)
print(f"LLVM type for void: {default_model.get_value_type()}")
# Output: LLVM type for void: i8*
# 2. CUDA Context return types also reflect this mismatch
ctx = cuda_target.target_context
print(f"Context return type: {ctx.get_return_type(types.void)}")
# Output: Context return type: i8*
# 3. Demonstrate this in a compiled CUDA device function
@cuda.jit(device=True)
def foo():
pass
@cuda.jit
def kernel():
foo()
kernel[1, 1]()
# The device function signature takes an `i8**` return pointer instead of returning `void`
for k, v in foo.inspect_llvm().items():
for line in v.splitlines():
if line.startswith("define") and "foo" in line:
print(line)
# Output: define linkonce_odr i32 @"_ZN8__main__3foo..."(i8** %".ret")
Expected Behavior
types.void should be mapped to a model that returns ir.VoidType() (i.e., void in LLVM IR), so that functions returning void have the correct LLVM IR signature without an unnecessary return pointer argument:
define void @"_ZN8__main__3foo..."()
And context return types should reflect void instead of i8*.
Actual Behavior
types.void is mapped to OpaqueModel, which returns ir.IntType(8).as_pointer() (ptr / i8*).
Because of this, the calling convention treats the return type as a pointer, generating an i8** return argument:
define linkonce_odr i32 @"_ZN8__main__3foo..."(i8** %".ret")
Impact
When exporting Numba JIT code to LLVM IR and linking it via LTO with external C/C++ device libraries, the LLVM linker detects a signature mismatch between the C++ declaration (void foo()) and the Numba definition (i32 foo(i8**)). This results in LTO linking failures and ABI correctness issues for CUDA device code.
Description
When using Numba's
types.void(e.g., as a return type for a device function or in signatures), Numba's datamodel translates it to an LLVMi8*(opaque pointerptrin LLVM 15+) instead of the actual LLVMvoidtype (ir.VoidType()).Note: This issue originates in the core
numbapackage itself (specifically innumba.core.datamodelwheretypes.voidis registered toOpaqueModel), but it has a severe impact onnumba-cudawhen linking device code.This causes the generated LLVM IR to treat
voidas a pointer type. In the Numba calling convention, this results in device functions taking ani8**return pointer argument rather than simply returningvoid. While this might accidentally work at runtime on some architectures, it breaks strict ABI requirements. More importantly, it causes fatal errors during Link Time Optimization (LTO) when linking Numba-generated IR with C/C++ device code that correctly declares the function as returningvoid.Reproducer
Expected Behavior
types.voidshould be mapped to a model that returnsir.VoidType()(i.e.,voidin LLVM IR), so that functions returningvoidhave the correct LLVM IR signature without an unnecessary return pointer argument:And context return types should reflect
voidinstead ofi8*.Actual Behavior
types.voidis mapped toOpaqueModel, which returnsir.IntType(8).as_pointer()(ptr/i8*).Because of this, the calling convention treats the return type as a pointer, generating an
i8**return argument:Impact
When exporting Numba JIT code to LLVM IR and linking it via LTO with external C/C++ device libraries, the LLVM linker detects a signature mismatch between the C++ declaration (
void foo()) and the Numba definition (i32 foo(i8**)). This results in LTO linking failures and ABI correctness issues for CUDA device code.