Skip to content

Commit 7a322dd

Browse files
FrancescAltedclaude
andcommitted
Address the Copilot review on PR #684
Three findings, all real: - @blosc2.jit forwarded every decorator kwarg to blosc2.asarray() when the traced function returned a NumPy array, so combining a storage kwarg with an execution-tuning one -- @blosc2.jit(jit=False, cparams=...) -- raised instead of returning an NDArray. Only storage kwargs go there now; the function has already run, so there is nothing left to tune. The sibling compute() call keeps taking all of them on purpose: compute() names fp_accuracy while lazyudf() does not, so stripping them would drop it. - The mandelbrot benchmark's header still said any non-None jit() kwarg flips the return container. Only storage kwargs do. - _fill_me_udata leaked np_data/np_typesizes on every allocation-failure exit after the one that allocates them. All the exits now go through one _free_me_udata_tables() helper, so a table added later cannot be missed by some of them again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent f5838d5 commit 7a322dd

5 files changed

Lines changed: 56 additions & 17 deletions

File tree

‎RELEASE_NOTES.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,12 @@ XXX version-specific blurb XXX
166166

167167
### Bug fixes
168168

169+
- **`@blosc2.jit` raised when a storage kwarg and an execution-tuning kwarg
170+
were combined** and the decorated function returned a NumPy array —
171+
`@blosc2.jit(jit=False, cparams=...)` ended in
172+
`blosc2.asarray(retval, jit=False, ...)`, which rejects the tuning kwargs.
173+
Only storage kwargs reach `asarray()` now; the function has already run, so
174+
there is nothing left to tune.
169175
- **A DSL kernel over a utf8 column registered as a computed column, then
170176
broke the table.** `add_computed_column(name, kernel, inputs=["utf8_col"])`
171177
was accepted, after which every read of that column *and* `str(table)`

‎bench/ndarray/jit-dsl-mandelbrot.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
# doc/guides/optimization_tips.md ("Let @blosc2.jit compile control flow
1313
# instead of tracing it").
1414
#
15-
# Return paths are equalized (both calls end in a plain NumPy array): any
16-
# non-None jit() kwarg flips the return from `retval[()]` to `.compute()`,
17-
# which would otherwise skew the comparison.
15+
# Return paths are equalized (both calls end in a plain NumPy array): a storage
16+
# kwarg (cparams/chunks/urlpath/...) flips the return from `retval[()]` to
17+
# `.compute()`, i.e. to an NDArray, which would otherwise skew the comparison.
18+
# Execution-tuning kwargs (jit/jit_backend/fp_accuracy) do not, so `@blosc2.jit`
19+
# is used bare here.
1820

1921
from __future__ import annotations
2022

‎src/blosc2/blosc2_ext.pyx‎

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,6 +1027,22 @@ def me_output_dtype(expression, operands):
10271027
free(variables)
10281028

10291029

1030+
cdef inline void _free_me_udata_tables(me_udata* udata, b2nd_array_t** inputs_,
1031+
uint8_t** np_data, int32_t* np_typesizes):
1032+
"""Release the per-input tables and the udata block itself.
1033+
1034+
``_fill_me_udata`` has half a dozen allocation-failure exits; routing them
1035+
all through here is what keeps the tables from drifting apart, which they
1036+
already had once -- ``np_data``/``np_typesizes`` arrived later and every
1037+
exit but the first kept freeing only ``inputs``. ``free(NULL)`` is a no-op,
1038+
so no exit needs to know which tables it got as far as allocating.
1039+
"""
1040+
free(inputs_)
1041+
free(np_data)
1042+
free(np_typesizes)
1043+
free(udata)
1044+
1045+
10301046
cdef inline str _me_compile_status_name(int rc):
10311047
if rc == ME_COMPILE_SUCCESS:
10321048
return "ME_COMPILE_SUCCESS"
@@ -4155,10 +4171,7 @@ cdef class NDArray:
41554171
np_data = <uint8_t**> calloc(ninputs, sizeof(uint8_t*))
41564172
np_typesizes = <int32_t*> calloc(ninputs, sizeof(int32_t))
41574173
if np_data == NULL or np_typesizes == NULL:
4158-
free(inputs_)
4159-
free(np_data)
4160-
free(np_typesizes)
4161-
free(udata)
4174+
_free_me_udata_tables(udata, inputs_, np_data, np_typesizes)
41624175
raise MemoryError("Cannot allocate miniexpr raw-input tables")
41634176
for i, operand in enumerate(operands):
41644177
if isinstance(operand, np.ndarray):
@@ -4176,8 +4189,7 @@ cdef class NDArray:
41764189
if ninputs > 0:
41774190
input_chunk_caches = <me_input_cache_s*> calloc(ninputs, sizeof(me_input_cache_s))
41784191
if input_chunk_caches == NULL:
4179-
free(inputs_)
4180-
free(udata)
4192+
_free_me_udata_tables(udata, inputs_, np_data, np_typesizes)
41814193
raise MemoryError("Cannot allocate miniexpr chunk caches")
41824194
for i in range(ninputs):
41834195
input_chunk_caches[i].nchunk = -1
@@ -4191,8 +4203,7 @@ cdef class NDArray:
41914203
if input_chunk_caches[i].ready_lock != NULL:
41924204
PyThread_free_lock(input_chunk_caches[i].ready_lock)
41934205
free(input_chunk_caches)
4194-
free(inputs_)
4195-
free(udata)
4206+
_free_me_udata_tables(udata, inputs_, np_data, np_typesizes)
41964207
raise MemoryError("Cannot allocate miniexpr chunk cache state lock")
41974208
input_chunk_caches[i].ready_lock = PyThread_allocate_lock()
41984209
if input_chunk_caches[i].ready_lock == NULL:
@@ -4205,8 +4216,7 @@ cdef class NDArray:
42054216
if input_chunk_caches[i].ready_lock != NULL:
42064217
PyThread_free_lock(input_chunk_caches[i].ready_lock)
42074218
free(input_chunk_caches)
4208-
free(inputs_)
4209-
free(udata)
4219+
_free_me_udata_tables(udata, inputs_, np_data, np_typesizes)
42104220
raise MemoryError("Cannot allocate miniexpr chunk cache ready lock")
42114221
udata.input_chunk_caches = input_chunk_caches
42124222
eval_params = <me_eval_params*> malloc(sizeof(me_eval_params))
@@ -4217,8 +4227,7 @@ cdef class NDArray:
42174227
if input_chunk_caches[i].ready_lock != NULL:
42184228
PyThread_free_lock(input_chunk_caches[i].ready_lock)
42194229
free(input_chunk_caches)
4220-
free(inputs_)
4221-
free(udata)
4230+
_free_me_udata_tables(udata, inputs_, np_data, np_typesizes)
42224231
raise MemoryError("Cannot allocate miniexpr eval params")
42234232
eval_params.disable_simd = False
42244233
eval_params.simd_ulp_mode = ME_SIMD_ULP_3_5 if fp_accuracy == blosc2.FPAccuracy.MEDIUM else ME_SIMD_ULP_1

‎src/blosc2/proxy.py‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1073,6 +1073,9 @@ def dsl_wrapper(*args, **func_kwargs):
10731073
return out
10741074

10751075
if storage_kwargs and any(v is not None for v in storage_kwargs.values()):
1076+
# Execution-tuning kwargs go along too: compute() names all three,
1077+
# while lazyudf() above only names jit/jit_backend, so fp_accuracy
1078+
# would otherwise be dropped on this path.
10761079
return lexpr.compute(**decorator_kwargs)
10771080
return lexpr[()]
10781081

@@ -1270,8 +1273,11 @@ def wrapper(*args, **func_kwargs):
12701273
# If it is a numpy array, return it as is
12711274
if isinstance(retval, np.ndarray):
12721275
if storage_kwargs and any(v is not None for v in storage_kwargs.values()):
1273-
# But if storage kwargs are provided, return a NDArray instead
1274-
return blosc2.asarray(retval, **kwargs)
1276+
# But if storage kwargs are provided, return a NDArray instead.
1277+
# Only storage kwargs: asarray() rejects the execution-tuning
1278+
# ones, and there is nothing left to tune -- the function has
1279+
# already run.
1280+
return blosc2.asarray(retval, **storage_kwargs)
12751281
return retval
12761282

12771283
# In some instances, the return value is not a LazyExpr

‎tests/ndarray/test_jit.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,19 @@ def f(a, b):
205205
assert isinstance(res, blosc2.NDArray)
206206
assert res.schunk.cparams.clevel == 2
207207
np.testing.assert_allclose(res[:], a * 2.0 + b)
208+
209+
210+
def test_jit_numpy_return_with_storage_and_tuning_kwargs():
211+
# A traced function whose return is already a NumPy array takes the
212+
# asarray() branch, which accepts storage kwargs only -- forwarding the
213+
# execution-tuning ones there raised instead of returning an NDArray.
214+
@blosc2.jit(jit=False, cparams=blosc2.CParams(clevel=2))
215+
def f(a, b):
216+
return np.sum(a * 2.0 + b, axis=0)
217+
218+
a = np.arange(1000, dtype=np.float64).reshape(10, 100)
219+
b = np.arange(1000, dtype=np.float64).reshape(10, 100) * 0.5
220+
res = f(a, b)
221+
assert isinstance(res, blosc2.NDArray)
222+
assert res.schunk.cparams.clevel == 2
223+
np.testing.assert_allclose(res[:], np.sum(a * 2.0 + b, axis=0))

0 commit comments

Comments
 (0)