Skip to content

Commit 9203ea3

Browse files
authored
Add Python module support for multi-dimensional arrays (#26940)
Adds Python module support for passing multi-dimensional Chapel arrays by reference and receiving multi-dimensional Python arrays. This is done by expanding our usage of the Python array buffer protocol, both as a producer (Chapel arrays by ref) and as a consumer (PyArray). This PR contains a number of additional fixes for issues I found along the way, including some portability issues. I also updated some tests (bs4) to be cleaner and use nicer syntax. - [x] `start_test test/library/packages/Python` with COMM=none - [x] `start_test test/library/packages/Python` with COMM=gasnet - [x] `start_test test/library/packages/Python --memLeaks` with COMM=none Future Work (#27073): - Support native indexing/slicing of Chapel Array Shim objects for ND arrays - Currently uses need to do something like `np.asarray` first, and then index the numpy view of the Chapel array - Print the shape of a Chapel Array Shim Object as apart of its string repr. - Expose the shape of a Chapel Array Shim Object as a property of the class via PyGetSetDef. - Consider supporting writable buffers [Reviewed by @lydia-duncan]
2 parents f53b87f + 07ec7b1 commit 9203ea3

38 files changed

+2223
-159
lines changed

modules/packages/Python.chpl

+425-68
Large diffs are not rendered by default.

modules/packages/PythonHelper/ArrayTypes.c

+82-13
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,10 @@ chpl_ARRAY_TYPES(chpl_MAKE_ENUM)
7474
DATATYPE* data; \
7575
Py_ssize_t size; \
7676
chpl_bool isOwned; \
77+
Py_ssize_t numExports; \
7778
PyObject* eltType; \
7879
Py_ssize_t ndim; \
80+
Py_ssize_t* shape; \
7981
} Array##NAMESUFFIX##Object; \
8082
PyTypeObject* Array##NAMESUFFIX##Type = NULL;
8183
chpl_ARRAY_TYPES(chpl_MAKE_ARRAY_STRUCT)
@@ -87,6 +89,7 @@ static int ArrayGenericObject_init(PyObject* self, PyObject* args, PyObject* kwa
8789
#define chpl_MAKE_DEALLOC(DATATYPE, CHAPELDATATYPE, NAMESUFFIX, ...) \
8890
static void Array##NAMESUFFIX##Object_dealloc(Array##NAMESUFFIX##Object* self) { \
8991
if (self->isOwned) chpl_mem_free(self->data, 0, 0); \
92+
chpl_mem_free(self->shape, 0, 0); \
9093
Py_CLEAR(self->eltType); \
9194
void (*tp_free)(PyObject*) = (void (*)(PyObject*))(PyType_GetSlot(Array##NAMESUFFIX##Type, Py_tp_free)); \
9295
if (!tp_free) PyErr_SetString(PyExc_RuntimeError, "Could not free object"); \
@@ -98,14 +101,19 @@ chpl_ARRAY_TYPES(chpl_MAKE_DEALLOC)
98101

99102
#define chpl_MAKE_REPR(DATATYPE, CHAPELDATATYPE, NAMESUFFIX, ...) \
100103
static PyObject* Array##NAMESUFFIX##Object_repr(Array##NAMESUFFIX##Object* self) { \
101-
return PyUnicode_FromFormat("Array(eltType=" CHAPELDATATYPE ",size=%zd)", self->size); \
104+
/* TODO: print the shape */ \
105+
return PyUnicode_FromFormat("Array(eltType=" CHAPELDATATYPE ",size=%zd,ndim=%zd)", self->size, self->ndim); \
102106
}
103107
chpl_ARRAY_TYPES(chpl_MAKE_REPR)
104108
#undef chpl_MAKE_REPR
105109

106110
// TODO: how can we remove the error checking here with --no-checks or -scheckExceptions=false?
107111
#define chpl_MAKE_SETITEM(DATATYPE, CHAPELDATATYPE, NAMESUFFIX, CHECKTYPE, ASTYPE, ...) \
108112
static int Array##NAMESUFFIX##Object_sq_setitem(Array##NAMESUFFIX##Object* self, Py_ssize_t index, PyObject* value) { \
113+
if (self->ndim != 1) { \
114+
PyErr_SetString(PyExc_TypeError, "can only assign to 1D arrays"); \
115+
return -1; \
116+
} \
109117
if (!value) { \
110118
PyErr_SetString(PyExc_TypeError, "cannot delete items from this array"); \
111119
return -1; \
@@ -128,6 +136,10 @@ chpl_ARRAY_TYPES(chpl_MAKE_SETITEM)
128136

129137
#define chpl_MAKE_SETITEM(DATATYPE, CHAPELDATATYPE, NAMESUFFIX, CHECKTYPE, ASTYPE, ...) \
130138
static int Array##NAMESUFFIX##Object_mp_setitem(Array##NAMESUFFIX##Object* self, PyObject* indexObj, PyObject* value) { \
139+
if (self->ndim != 1) { \
140+
PyErr_SetString(PyExc_TypeError, "can only assign to 1D arrays"); \
141+
return -1; \
142+
} \
131143
if (!value) { \
132144
PyErr_SetString(PyExc_TypeError, "cannot delete items from this array"); \
133145
return -1; \
@@ -156,6 +168,10 @@ chpl_ARRAY_TYPES(chpl_MAKE_SETITEM)
156168

157169
#define chpl_MAKE_GETITEM(DATATYPE, CHAPELDATATYPE, NAMESUFFIX, CHECKTYPE, ASTYPE, FROMTYPE, ...) \
158170
static PyObject* Array##NAMESUFFIX##Object_sq_getitem(Array##NAMESUFFIX##Object* self, Py_ssize_t index) { \
171+
if (self->ndim != 1) { \
172+
PyErr_SetString(PyExc_TypeError, "can only get items from 1D arrays"); \
173+
return NULL; \
174+
} \
159175
if (index < 0 || index >= self->size) { \
160176
PyErr_SetString(PyExc_IndexError, "index out of bounds"); \
161177
return NULL; \
@@ -166,6 +182,10 @@ chpl_ARRAY_TYPES(chpl_MAKE_GETITEM)
166182
#undef chpl_MAKE_GETITEM
167183
#define chpl_MAKE_GETITEM(DATATYPE, CHAPELDATATYPE, NAMESUFFIX, CHECKTYPE, ASTYPE, FROMTYPE, ...) \
168184
static PyObject* Array##NAMESUFFIX##Object_mp_getitem(Array##NAMESUFFIX##Object* self, PyObject* indexObj) { \
185+
if (self->ndim != 1) { \
186+
PyErr_SetString(PyExc_TypeError, "can only get items from 1D arrays"); \
187+
return NULL; \
188+
} \
169189
if (!PyLong_Check(indexObj)) { \
170190
/* TODO: support slices */ \
171191
PyErr_SetString(PyExc_TypeError, "index must be an integer"); \
@@ -183,6 +203,10 @@ chpl_ARRAY_TYPES(chpl_MAKE_GETITEM)
183203

184204
#define chpl_MAKE_LENGTH(DATATYPE, CHAPELDATATYPE, NAMESUFFIX, ...) \
185205
static Py_ssize_t Array##NAMESUFFIX##Object_length(Array##NAMESUFFIX##Object* self) { \
206+
if (self->ndim != 1) { \
207+
PyErr_SetString(PyExc_TypeError, "can only get the length of 1D arrays"); \
208+
return -1; \
209+
} \
186210
return self->size; \
187211
}
188212
chpl_ARRAY_TYPES(chpl_MAKE_LENGTH)
@@ -202,27 +226,64 @@ static int Array##NAMESUFFIX##Object_bf_getbuffer(Array##NAMESUFFIX##Object* arr
202226
view->obj = NULL; \
203227
return -1; \
204228
} \
205-
/* other error checking for invalid request in flags?! */ \
229+
assert(arr->ndim != 0); \
230+
/* check for any unsupported flags */ \
231+
if (flags & ~(PyBUF_SIMPLE | PyBUF_WRITABLE | PyBUF_FORMAT | \
232+
PyBUF_ND | PyBUF_STRIDES | PyBUF_INDIRECT | PyBUF_C_CONTIGUOUS)) { \
233+
PyErr_Format(PyExc_BufferError, "Unsupported buffer flags: 0x%x", flags); \
234+
view->obj = NULL; \
235+
return -1; \
236+
} \
206237
view->buf = arr->data; \
207-
view->obj = (PyObject*) arr; \
238+
view->internal = 0; \
208239
view->len = arr->size * sizeof(DATATYPE); \
209240
view->itemsize = sizeof(DATATYPE); \
210-
view->readonly = 0; \
241+
view->readonly = 0; /* the buffer is always writeable */ \
211242
view->ndim = arr->ndim; \
212-
view->format = NULL; \
213-
if (flags & PyBUF_FORMAT) { \
214-
view->format = (char*)FORMATSTRING; \
243+
if (flags & PyBUF_FORMAT) { view->format = (char*)FORMATSTRING; } \
244+
else { view->format = NULL; } \
245+
if (flags & PyBUF_ND) { view->shape = arr->shape; } \
246+
else { view->shape = NULL; } \
247+
if (flags & PyBUF_STRIDES) { \
248+
view->strides = (Py_ssize_t*)chpl_mem_alloc(sizeof(Py_ssize_t) * arr->ndim, 0, 0, 0); \
249+
if (!view->strides) { \
250+
PyErr_SetString(PyExc_MemoryError, "Could not allocate strides"); \
251+
view->obj = NULL; \
252+
return -1; \
253+
} \
254+
if (arr->ndim == 1) { \
255+
view->strides[0] = sizeof(DATATYPE); \
256+
} else { \
257+
view->strides[arr->ndim-1] = sizeof(DATATYPE); \
258+
for (Py_ssize_t i = arr->ndim - 2; i >= 0; i--) { \
259+
view->strides[i] = view->strides[i+1] * arr->shape[i+1]; \
260+
} \
261+
} \
262+
view->internal = (void*)((intptr_t)view->internal | 0x1); /* set 0x1 if we need to free strides later */ \
215263
} \
216-
/*TODO: support nd arrays*/ \
217-
view->shape = NULL; \
218-
view->strides = NULL; \
219-
view->suboffsets = NULL; \
264+
else { view->strides = NULL; } \
265+
view->suboffsets = NULL; /*always NULL, even if PyBUF_INDIRECT is set*/ \
266+
arr->numExports++; \
267+
view->obj = (PyObject*) arr; \
220268
Py_INCREF(view->obj); \
221269
return 0; \
222270
}
223271
chpl_ARRAY_TYPES(chpl_MAKE_GET_BUFFER)
224272
#undef chpl_MAKE_GET_BUFFER
225273

274+
#define chpl_MAKE_RELEASE_BUFFER(DATATYPE, CHAPELDATATYPE, NAMESUFFIX, _0, _1, _2, SUPPORTSBUFFERS, FORMATSTRING) \
275+
static void Array##NAMESUFFIX##Object_bf_releasebuffer(Array##NAMESUFFIX##Object* arr, Py_buffer* view) { \
276+
arr->numExports--; \
277+
if ((intptr_t)view->internal & 0x1) { \
278+
/* free the strides */ \
279+
chpl_mem_free(view->strides, 0, 0); \
280+
view->internal = (void*)((intptr_t)view->internal & ~0x1); \
281+
} \
282+
}
283+
chpl_ARRAY_TYPES(chpl_MAKE_RELEASE_BUFFER)
284+
#undef chpl_MAKE_RELEASE_BUFFER
285+
286+
226287
#if PY_VERSION_HEX >= 0x030a0000 /* Python 3.10 */
227288
#define chpl_Py_TPFLAGS_SEQUENCE Py_TPFLAGS_SEQUENCE
228289
#else
@@ -242,6 +303,7 @@ chpl_ARRAY_TYPES(chpl_MAKE_GET_BUFFER)
242303
{"ndim", Py_T_PYSSIZET, offsetof(Array##NAMESUFFIX##Object, ndim), Py_READONLY, PyDoc_STR("number of dimensions in the array")}, \
243304
{NULL} /* Sentinel */\
244305
}; \
306+
/* TODO: expose shape as a `PyGetSetDef` */ \
245307
PyType_Slot slots[] = { \
246308
{Py_tp_init, (void*) ArrayGenericObject_init}, \
247309
{Py_tp_dealloc, (void*) Array##NAMESUFFIX##Object_dealloc}, \
@@ -256,6 +318,7 @@ chpl_ARRAY_TYPES(chpl_MAKE_GET_BUFFER)
256318
{Py_tp_methods, (void*) methods}, \
257319
{Py_tp_members, (void*) members}, \
258320
{Py_bf_getbuffer, (void*) Array##NAMESUFFIX##Object_bf_getbuffer}, \
321+
{Py_bf_releasebuffer, (void*) Array##NAMESUFFIX##Object_bf_releasebuffer}, \
259322
{0, NULL} \
260323
}; \
261324
PyType_Spec spec = { \
@@ -281,7 +344,11 @@ chpl_ARRAY_TYPES(chpl_MAKE_TYPE)
281344
}
282345

283346
#define chpl_CREATE_ARRAY(DATATYPE, CHAPELDATATYPE, NAMESUFFIX, ...) \
284-
PyObject* createArray##NAMESUFFIX(DATATYPE* data, Py_ssize_t size, chpl_bool isOwned) { \
347+
PyObject* createArray##NAMESUFFIX(DATATYPE* data, \
348+
Py_ssize_t size, \
349+
Py_ssize_t ndim, \
350+
Py_ssize_t* shape, \
351+
chpl_bool isOwned) { \
285352
assert(Array##NAMESUFFIX##Type); \
286353
assert(ArrayTypeEnum); \
287354
PyObject* objPy = PyObject_CallNoArgs((PyObject *) Array##NAMESUFFIX##Type); \
@@ -290,8 +357,10 @@ chpl_ARRAY_TYPES(chpl_MAKE_TYPE)
290357
obj->data = data; \
291358
obj->size = size; \
292359
obj->isOwned = isOwned; \
360+
obj->numExports = 0; \
293361
obj->eltType = PyObject_GetAttrString(ArrayTypeEnum, #NAMESUFFIX); \
294-
obj->ndim = 1; /*TODO: when we support proper ND chapel arrays, set dynamicly */\
362+
obj->ndim = ndim; \
363+
obj->shape = shape; \
295364
return objPy; \
296365
}
297366
chpl_ARRAY_TYPES(chpl_CREATE_ARRAY)

modules/packages/PythonHelper/ArrayTypes.h

+9-3
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
#include <stdint.h>
2626
#include "chpltypes.h"
2727

28-
// TODO: for some of the smaller types, when comsuming a PyObject we should check
28+
// TODO: for some of the smaller types, when consuming a PyObject we should check
2929
// if it will fit in the C type, and if not raise an error.
3030
//
3131
// args:
@@ -53,14 +53,20 @@
5353
V(PyObject*, "array", A, (intptr_t), (PyObject*), (PyObject*), false, "P")
5454

5555

56-
#define chpl_MAKE_ARRAY_TYPES(DATATYPE, CHAPELDATATYPE, NAMESUFFIX, ...) extern PyTypeObject* Array##NAMESUFFIX##Type;
56+
#define chpl_MAKE_ARRAY_TYPES(DATATYPE, CHAPELDATATYPE, NAMESUFFIX, ...) \
57+
extern PyTypeObject* Array##NAMESUFFIX##Type;
5758
chpl_ARRAY_TYPES(chpl_MAKE_ARRAY_TYPES)
5859
#undef chpl_MAKE_ARRAY_TYPES
5960

6061
chpl_bool registerArrayTypeEnum(void);
6162
chpl_bool createArrayTypes(void);
6263

63-
#define chpl_CREATE_ARRAY(DATATYPE, CHAPELDATATYPE, NAMESUFFIX, ...) PyObject* createArray##NAMESUFFIX(DATATYPE* data, Py_ssize_t size, chpl_bool isOwned);
64+
#define chpl_CREATE_ARRAY(DATATYPE, CHAPELDATATYPE, NAMESUFFIX, ...) \
65+
PyObject* createArray##NAMESUFFIX(DATATYPE* data, \
66+
Py_ssize_t numElements, \
67+
Py_ssize_t ndim, \
68+
Py_ssize_t* shape, \
69+
chpl_bool isOwned);
6470
chpl_ARRAY_TYPES(chpl_CREATE_ARRAY)
6571
#undef chpl_CREATE_ARRAY
6672

modules/packages/PythonHelper/ChapelPythonHelper.h

+3
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ static inline int chpl_PyGen_Check(PyObject* o) { return PyGen_Check(o); }
7373
static inline PyObject* chpl_Py_None(void) { return (PyObject*)Py_None; }
7474
static inline PyObject* chpl_Py_True(void) { return (PyObject*)Py_True; }
7575
static inline PyObject* chpl_Py_False(void) { return (PyObject*)Py_False; }
76+
static inline PyObject* chpl_Py_NotImplemented(void) { return (PyObject*)Py_NotImplemented; }
7677

7778
static inline PyObject* chpl_Py_CompileString(const char* str,
7879
const char* filename, int start) {
@@ -100,5 +101,7 @@ static const int chpl_PyBUF_SIMPLE = PyBUF_SIMPLE;
100101
static const int chpl_PyBUF_WRITABLE = PyBUF_WRITABLE;
101102
static const int chpl_PyBUF_FORMAT = PyBUF_FORMAT;
102103
static const int chpl_PyBUF_ND = PyBUF_ND;
104+
static const int chpl_PyBUF_STRIDES = PyBUF_STRIDES;
105+
static const int chpl_PyBUF_C_CONTIGUOUS = PyBUF_C_CONTIGUOUS;
103106

104107
#endif
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
[[[0 0 0]
2+
[0 0 0]
3+
[0 0 0]]
4+
5+
[[0 0 0]
6+
[0 0 0]
7+
[0 0 0]]
8+
9+
[[0 0 0]
10+
[0 0 0]
11+
[0 0 0]]]
12+
[[0. 0.]
13+
[0. 0.]]
14+
[0. 0. 0. 0.]
15+
Caught error: ChapelException: Cannot index a 3-dimensional array with a 2-dimensional index
16+
Caught error: ChapelException: Index out of bounds
17+
Caught error: ChapelException: Index out of bounds
18+
Caught error: ChapelException: Source array's format does not match requested element type
19+
Caught error: ChapelException: Source array's format does not match requested element type
20+
Caught error: ChapelException: Source array's format does not match requested element type
21+
Caught error: ChapelException: Cannot index a 2-dimensional array with a 1-dimensional index
22+
Caught error: ChapelException: Rank mismatch: expected 2 dimensions, but got 1
23+
Caught error: ChapelException: Cannot index a 1-dimensional array with a 2-dimensional index
24+
Caught error: ChapelException: Source array's format does not match requested element type
25+
Caught error: ChapelException: Python array of rank 3 cannot be converted to a Chapel array of rank 1
26+
Caught error: ChapelException: 0-dimensional arrays are not supported
27+
Caught error: ChapelException: Cannot index a 2-dimensional array with a 1-dimensional index
28+
Caught error: ChapelException: Cannot index a 3-dimensional array with a 1-dimensional index
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
$CHPL_HOME/modules/packages/Python.chpl:nnnn: In method 'array':
2+
$CHPL_HOME/modules/packages/Python.chpl:nnnn: error: Rank must be specified at compile time
3+
arrayErrors.chpl:145: called as (PyArray(nothing,-1)).array(type eltType = int(64))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
arrayErrors.chpl:5: In function 'main':
2+
arrayErrors.chpl:148: error: Rank must be specified at compile time
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
arrayErrors.chpl:5: In function 'main':
2+
arrayErrors.chpl:151: error: Attempting to index an array of rank 2 with a 1-dimensional index
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
$CHPL_HOME/modules/packages/Python.chpl:nnnn: In initializer:
2+
$CHPL_HOME/modules/packages/Python.chpl:nnnn: error: Unsupported array type: imag(64)
3+
arrayErrors.chpl:118: called as (Array(?)).init(interpreter: borrowed Interpreter, arr: [domain(1,int(64),one)] imag(64))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
$CHPL_HOME/modules/packages/Python.chpl:nnnn: In initializer:
2+
$CHPL_HOME/modules/packages/Python.chpl:nnnn: error: Unsupported array type: complex(128)
3+
arrayErrors.chpl:122: called as (Array(?)).init(interpreter: borrowed Interpreter, arr: [domain(1,int(64),one)] complex(128))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
$CHPL_HOME/modules/packages/Python.chpl:nnnn: In method 'this':
2+
$CHPL_HOME/modules/packages/Python.chpl:nnnn: error: Element type must be specified at compile time
3+
arrayErrors.chpl:125: called as (PyArray(nothing,-1)).this(idx: int(64))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
$CHPL_HOME/modules/packages/Python.chpl:nnnn: In method 'this':
2+
$CHPL_HOME/modules/packages/Python.chpl:nnnn: error: Attempting to index an array of rank 3 with a 1-dimensional index
3+
arrayErrors.chpl:128: called as (PyArray(int(64),3)).this(idx: int(64))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
arrayErrors.chpl:5: In function 'main':
2+
arrayErrors.chpl:131: error: Attempting to index an array of rank 3 with a 2-dimensional index
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
arrayErrors.chpl:5: In function 'main':
2+
arrayErrors.chpl:134: error: Invalid index type of '(int(64),int(64),real(64))' for array - index must be a single int (for 1D arrays only) or a tuple of ints
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
$CHPL_HOME/modules/packages/Python.chpl:nnnn: In iterator 'these':
2+
$CHPL_HOME/modules/packages/Python.chpl:nnnn: error: Element type must be specified at compile time
3+
$CHPL_HOME/modules/internal/ChapelIteratorSupport.chpl:nnnn: called as (PyArray(nothing,-1)).these()
4+
within internal functions (use --print-callstack-on-error to see)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
$CHPL_HOME/modules/packages/Python.chpl:nnnn: In method 'array':
2+
$CHPL_HOME/modules/packages/Python.chpl:nnnn: error: Element type must be specified at compile time
3+
arrayErrors.chpl:142: called as (PyArray(nothing,-1)).array()

0 commit comments

Comments
 (0)