Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions src/awkward/_connect/numba/arrayview.py
Original file line number Diff line number Diff line change
Expand Up @@ -1068,18 +1068,24 @@ def lower_asarray(context, builder, sig, args):
whichpos = ak._connect.numba.layout.posat(
context, builder, viewproxy.pos, viewtype.type.ARRAY
)

arrayptr = ak._connect.numba.layout.getat(
context, builder, viewproxy.arrayptrs, whichpos
)

bitwidth = ak._connect.numba.layout.type_bitwidth(rettype.dtype)
itemsize = context.get_constant(numba.intp, bitwidth // 8)

data = numba.core.cgutils.pointer_add(
builder,
data_pointer_type = context.get_value_type(numba.types.CPointer(rettype.dtype))

typed_arrayptr = builder.inttoptr(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is the inttoptr necessary? In numba, we have seen LLVM loop-unrolling passes that ignores the pointer provenance from inttoptr. I highly recommend avoiding it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I saw

("arrayptrs", numba.types.CPointer(numba.intp)),

Can you model arrayptrs as i8** (so like a void**) instead of intp*?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks @sklam ! One thing I'd like to clarify is the scope of this PR change.

This PR updates Awkward to explicitly reconstruct typed pointers with inttoptr before performing pointer arithmetic with getelementptr, making it compatible with Numba's recent changes to pointer_add.

However, I don't think we should describe this as fully solving pointer provenance. Awkward's lookup table still stores buffer addresses as intp values, so the original pointer provenance has already been discarded before the lowering reconstructs a pointer. This PR improves correctness relative to the new Numba implementation, but it doesn't eliminate the underlying design issue.

I think preserving pointer provenance end-to-end would require a larger redesign of the lookup representation (for example, separating integer layout metadata from a table of actual buffer pointers). I've opened a follow-up issue to discuss that independently: #4252

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I'm recommending extra caution here because:

a) The pointer provenance issue is complicated by the uncertain state in LLVM. It is a LLVM design bug that is being addressed. See https://www.npopov.com/2026/01/31/This-year-in-LLVM-2025.html#ptrtoaddr .
b) The effect from it is very difficult to predict, replicate or track down. Undefined behavior gives LLVM the license to do very unusual things (see LLVM blog). For example, Numba saw the issue to only appear in a very particular loop nest pattern and on a specific CPU ISA due to LLVM's target cost model.

There are more details from my debugging in the original Numba issue here: numba/numba#10695 (comment).

All these made me scared about having inttoptrand ptrtoint.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks @sklam ! You are right it is a dangerous territory! I ought to look into a safer fix.

arrayptr,
builder.mul(viewproxy.start, itemsize),
context.get_value_type(numba.types.CPointer(rettype.dtype)),
data_pointer_type,
)

data = builder.gep(
typed_arrayptr,
[viewproxy.start],
)

shape = context.make_tuple(
Expand Down
57 changes: 35 additions & 22 deletions src/awkward/_connect/numba/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import json

import llvmlite.ir
import numba
from numba.core.errors import NumbaTypeError, NumbaValueError

Expand Down Expand Up @@ -58,25 +57,30 @@ def string_numba_lower(
baseptr = ak._connect.numba.layout.getat(
context, builder, viewproxy.arrayptrs, whichnextpos
)
rawptr = builder.add(
baseptr,
ak._connect.numba.layout.castint(
context, builder, viewtype.type.indextype.dtype, numba.intp, start
),
)
rawptr_cast = builder.inttoptr(
rawptr,
llvmlite.ir.PointerType(llvmlite.ir.IntType(numba.intp.bitwidth // 8)),
start_cast = ak._connect.numba.layout.castint(
context, builder, viewtype.type.indextype.dtype, numba.intp, start
)
strsize = builder.sub(stop, start)
strsize_cast = ak._connect.numba.layout.castint(
context, builder, viewtype.type.indextype.dtype, numba.intp, strsize
)
# ``baseptr`` is stored in the Awkward lookup table as an integer
# address. Reconstruct a typed pointer explicitly before applying
# pointer arithmetic with GEP.
uint8_pointer_type = context.get_value_type(numba.types.CPointer(numba.types.uint8))
typed_baseptr = builder.inttoptr(
baseptr,
uint8_pointer_type,
)
rawptr = builder.gep(
typed_baseptr,
[start_cast],
)

pyapi = context.get_python_api(builder)
gil = pyapi.gil_ensure()

strptr = builder.bitcast(rawptr_cast, pyapi.cstring)
strptr = builder.bitcast(rawptr, pyapi.cstring)

if viewtype.type.parameters["__array__"] == "string":
kind = context.get_constant(numba.types.int32, pyapi.py_unicode_1byte_kind)
Expand Down Expand Up @@ -353,24 +357,33 @@ def posat(context, builder, pos, offset):


def getat(context, builder, baseptr, offset, rettype=None):
ptrtype = None
if rettype is not None:
ptrtype = context.get_value_type(numba.types.CPointer(rettype))
bitwidth = type_bitwidth(rettype)
else:
bitwidth = numba.intp.bitwidth
byteoffset = builder.mul(offset, context.get_constant(numba.intp, bitwidth // 8))
out = builder.load(
numba.core.cgutils.pointer_add(builder, baseptr, byteoffset, ptrtype)
"""
Load an element from an external buffer address.

Awkward's lookup table stores buffer pointers as integer addresses.
Convert the address explicitly to a typed pointer before using GEP.
"""
element_type = numba.intp if rettype is None else rettype
pointer_type = context.get_value_type(numba.types.CPointer(element_type))
typed_baseptr = builder.inttoptr(
baseptr,
pointer_type,
)

element_ptr = builder.gep(
typed_baseptr,
[offset],
)

out = builder.load(element_ptr)

if rettype is not None and isinstance(rettype, numba.types.Boolean):
return builder.icmp_signed(
"!=",
out,
context.get_constant(numba.int8, 0),
)
else:
return out
return out


def regularize_atval(context, builder, viewproxy, attype, atval, wrapneg, checkbounds):
Expand Down
68 changes: 68 additions & 0 deletions tests/test_4251_numba_intaddr_pointer_reconstruction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# BSD 3-Clause License; see https://github.com/scikit-hep/awkward/blob/main/LICENSE

from __future__ import annotations

import pytest

import awkward as ak

numba = pytest.importorskip("numba")

ak.numba.register_and_check()


@pytest.mark.parametrize(
"array",
[
ak.Array([True, False, True]),
ak.Array([1, 2, 3]),
ak.Array([1.5, 2.5, 3.5]),
ak.Array([[1, 2], [], [3]]),
ak.Array(["hello", "world"]),
ak.Array([b"hello", b"\x00\xff"]),
],
)
def test_numba_getitem_after_pointer_conversion(array):
@numba.njit
def getitem(array, index):
return array[index]

for index, expected in enumerate(array.to_list()):
assert ak.to_list(getitem(array, index)) == expected


def test_numba_ragged_string_membership():
array = ak.Array(
[
["TRA", "TRB"],
[],
None,
["TRG", "TRA"],
]
)

@numba.njit
def isin(array, haystack, builder):
for row in array:
builder.begin_list()

if row is not None:
for value in row:
builder.append(value in haystack)

builder.end_list()

return builder

result = isin(
array,
("TRA", "TRB"),
ak.ArrayBuilder(),
).snapshot()

assert result.to_list() == [
[True, True],
[],
[],
[False, True],
]
Loading