Skip to content

Commit 04f2577

Browse files
committed
Merge branch 'ctable4' of github.com:Blosc/python-blosc2 into ctable4
2 parents 465e855 + eaccd53 commit 04f2577

68 files changed

Lines changed: 20130 additions & 857 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ repos:
1616
- id: trailing-whitespace
1717

1818
- repo: https://github.com/astral-sh/ruff-pre-commit
19-
rev: v0.15.4
19+
rev: v0.15.9
2020
hooks:
2121
- id: ruff-check
2222
args: ["--fix", "--show-fixes"]

AGENTS.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Repository Guidelines
2+
3+
## Project Structure & Module Organization
4+
The Python package lives in `src/blosc2/`, including the C/Cython extension sources
5+
(`blosc2_ext.*`) and core modules such as `core.py`, `ndarray.py`, and `schunk.py`.
6+
Tests are under `tests/`, with additional doctests enabled for select modules per
7+
`pytest.ini`. Documentation sources are in `doc/` and build output lands in `html/`.
8+
Examples are in `examples/`, and performance/benchmark scripts live in `bench/`.
9+
10+
## Build, Test, and Development Commands
11+
- `pip install .` builds the bundled C-Blosc2 and installs the package.
12+
- `pip install -e .` installs in editable mode for local development.
13+
- `CMAKE_PREFIX_PATH=/usr/local USE_SYSTEM_BLOSC2=1 pip install -e .` builds
14+
against a separately installed C-Blosc2.
15+
- `pytest` runs the default test suite (excludes `heavy` and `network` markers).
16+
- `pytest -m "heavy"` runs long-running tests.
17+
- `pytest -m "network"` runs tests requiring network access.
18+
- `cd doc && rm -rf ../html _build && python -m sphinx . ../html` builds docs.
19+
20+
## Coding Style & Naming Conventions
21+
Use Ruff for formatting and linting (line length 109). Enable pre-commit hooks:
22+
`python -m pip install pre-commit` then `pre-commit install`. Follow Python
23+
conventions: 4-space indentation, `snake_case` for functions/variables, and
24+
`PascalCase` for classes. Pytest discovery expects `tests/test_*.py` and
25+
`test_*` functions. Do not use leading underscores in module-level helper
26+
function names when those helpers are imported from other modules; reserve
27+
leading underscores for file-local implementation details. Avoid leading
28+
underscores in core module filenames under `src/blosc2/`; prefer non-underscored
29+
module names unless there is a strong reason to keep a module private.
30+
31+
For documentation and tutorial query examples, prefer the shortest idiom that
32+
matches the intended result type. Use `expr[:]` or `arr[mask][:]` when showing
33+
values, use `expr.compute()` when materializing an `NDArray`, and use
34+
`expr.compute(_use_index=False)` when demonstrating scan-vs-index behavior.
35+
Avoid `expr.compute()[:]` unless a NumPy array is specifically required.
36+
37+
## Testing Guidelines
38+
Pytest is required; warnings are treated as errors. The default configuration
39+
adds `--doctest-modules`, so keep doctest examples in `blosc2/core.py`,
40+
`blosc2/ndarray.py`, and `blosc2/schunk.py` accurate. Use markers `heavy` and
41+
`network` for slow or network-dependent tests.
42+
43+
## Commit & Pull Request Guidelines
44+
Recent commit messages are short, imperative sentences (e.g., “Add …”, “Fix …”)
45+
without ticket prefixes. For pull requests: branch from `main`, add tests for
46+
behavior changes, update docs for API changes, ensure the test suite passes,
47+
and avoid introducing new compiler warnings. Link issues when applicable and
48+
include clear reproduction steps for bug fixes.
49+
50+
**IMPORTANT — Agent commit policy**: Never run `git commit`, `git push`,
51+
`git reset`, `git rebase`, or any other destructive/state-changing git operation.
52+
Do not ask for permission to do so either. Committing and all repo state changes
53+
are exclusively the user’s decision. Just do the code work and stop there.

CMakeLists.txt

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,25 @@ add_custom_command(
4141
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/blosc2/blosc2_ext.pyx"
4242
VERBATIM)
4343

44+
add_custom_command(
45+
OUTPUT indexing_ext.c
46+
COMMAND Python::Interpreter -m cython
47+
"${CMAKE_CURRENT_SOURCE_DIR}/src/blosc2/indexing_ext.pyx" --output-file indexing_ext.c
48+
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/blosc2/indexing_ext.pyx"
49+
VERBATIM)
50+
4451
# ...and add it to the target
4552
Python_add_library(blosc2_ext MODULE blosc2_ext.c WITH_SOABI)
53+
target_sources(blosc2_ext PRIVATE src/blosc2/matmul_kernels.c)
54+
target_include_directories(blosc2_ext PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/blosc2)
55+
if(UNIX)
56+
target_link_libraries(blosc2_ext PRIVATE ${CMAKE_DL_LIBS})
57+
endif()
58+
Python_add_library(indexing_ext MODULE indexing_ext.c WITH_SOABI)
4659

4760
# We need to link against NumPy
4861
target_link_libraries(blosc2_ext PRIVATE Python::NumPy)
62+
target_link_libraries(indexing_ext PRIVATE Python::NumPy)
4963

5064
# Fetch and build miniexpr library
5165
include(FetchContent)
@@ -63,15 +77,19 @@ endif()
6377

6478
FetchContent_Declare(miniexpr
6579
GIT_REPOSITORY https://github.com/Blosc/miniexpr.git
66-
GIT_TAG feadbc633a887bafd84b2fbc370ef2962d01b7ee
80+
GIT_TAG f2faef741c4c507bf6a03167c72ce7f92c6f0ae8
6781
# SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../miniexpr
6882
)
6983
FetchContent_MakeAvailable(miniexpr)
7084

7185
# Link against miniexpr static library
7286
target_link_libraries(blosc2_ext PRIVATE miniexpr_static)
87+
if(APPLE)
88+
target_link_libraries(blosc2_ext PRIVATE "-framework Accelerate")
89+
endif()
7390

7491
target_compile_features(blosc2_ext PRIVATE c_std_11)
92+
target_compile_features(indexing_ext PRIVATE c_std_11)
7593
if(WIN32 AND CMAKE_C_COMPILER_ID STREQUAL "Clang")
7694
execute_process(
7795
COMMAND "${CMAKE_C_COMPILER}" -print-resource-dir
@@ -119,7 +137,7 @@ else()
119137
include(FetchContent)
120138
FetchContent_Declare(blosc2
121139
GIT_REPOSITORY https://github.com/Blosc/c-blosc2
122-
GIT_TAG b32256fc1287b6e24c22f09ac202265c7054e2bc
140+
GIT_TAG 2fdba83b4046f22fed95a794d43a0376cce3d5db
123141
# SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../c-blosc2
124142
)
125143
FetchContent_MakeAvailable(blosc2)
@@ -148,7 +166,7 @@ endif()
148166

149167
# Python extension -> site-packages/blosc2
150168
install(
151-
TARGETS blosc2_ext
169+
TARGETS blosc2_ext indexing_ext
152170
LIBRARY DESTINATION ${SKBUILD_PLATLIB_DIR}/blosc2
153171
)
154172

README_DEVELOPERS.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,36 @@ If you want to run the network tests, you can use the following command:
9393
pytest -m "network"
9494
```
9595

96+
## Matmul backend discovery
97+
98+
The fast `blosc2.matmul` path uses platform-specific block kernels:
99+
100+
- macOS: `Accelerate`
101+
- Linux/Windows: runtime-discovered `cblas`
102+
- fallback: portable `naive` kernel
103+
104+
For the runtime `cblas` backend, `python-blosc2` probes the active Python/NumPy
105+
environment rather than linking to one BLAS vendor at build time. Discovery
106+
starts from NumPy's reported BLAS library directory when available, and then
107+
searches common library names in the active environment's `lib` directories.
108+
109+
On Linux the current candidates include `libcblas`, `libopenblas`,
110+
`libflexiblas`, `libblis`, `libmkl_rt`, and generic `libblas`. A candidate is
111+
accepted only if it loads successfully and exports both `cblas_sgemm` and
112+
`cblas_dgemm`. If no suitable provider is found, the fast path falls back to
113+
the `naive` kernel.
114+
115+
Useful runtime helpers:
116+
117+
- `blosc2.get_matmul_library()` reports the selected runtime library when available
118+
- `BLOSC_TRACE=1` logs candidate probing, rejection, selection, and backend fallback
119+
120+
Example:
121+
122+
```bash
123+
BLOSC_TRACE=1 python -c "import blosc2; print(blosc2.get_matmul_library())"
124+
```
125+
96126
## wasm32 / Pyodide developer workflow
97127

98128
For the local wasm32 workflow (uv + pyodide-build + cibuildwheel + test loop),
Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
# SPDX-License-Identifier: BSD-3-Clause
66
#######################################################################
77

8+
# This benchmarks BatchArray random single-item reads. It supports
9+
# msgpack or arrow, configurable codec/compression level, optional
10+
# dictionary compression, and in-memory vs persistent mode.
11+
812
from __future__ import annotations
913

1014
import argparse
@@ -15,7 +19,7 @@
1519
import blosc2
1620

1721

18-
URLPATH = "bench_batch_store.b2b"
22+
URLPATH = "bench_batch_array.b2b"
1923
NBATCHES = 10_000
2024
OBJECTS_PER_BATCH = 100
2125
TOTAL_OBJECTS = NBATCHES * OBJECTS_PER_BATCH
@@ -46,23 +50,23 @@ def expected_entry(batch_index: int, item_index: int) -> dict[str, int]:
4650

4751
def build_parser() -> argparse.ArgumentParser:
4852
parser = argparse.ArgumentParser(
49-
description="Benchmark BatchStore single-entry reads.",
53+
description="Benchmark BatchArray single-entry reads.",
5054
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
5155
)
5256
parser.add_argument("--codec", type=str, default="ZSTD", choices=[codec.name for codec in blosc2.Codec])
5357
parser.add_argument("--clevel", type=int, default=5)
5458
parser.add_argument("--serializer", type=str, default="msgpack", choices=["msgpack", "arrow"])
5559
parser.add_argument("--use-dict", action="store_true", help="Enable dictionaries for ZSTD/LZ4/LZ4HC codecs.")
56-
parser.add_argument("--in-mem", action="store_true", help="Keep the BatchStore purely in memory.")
60+
parser.add_argument("--in-mem", action="store_true", help="Keep the BatchArray purely in memory.")
5761
return parser
5862

5963

60-
def build_store(
64+
def build_array(
6165
codec: blosc2.Codec, clevel: int, use_dict: bool, serializer: str, in_mem: bool
62-
) -> blosc2.BatchStore | None:
66+
) -> blosc2.BatchArray | None:
6367
if in_mem:
6468
storage = blosc2.Storage(mode="w")
65-
store = blosc2.BatchStore(
69+
barr = blosc2.BatchArray(
6670
storage=storage,
6771
items_per_block=ITEMS_PER_BLOCK,
6872
serializer=serializer,
@@ -73,8 +77,8 @@ def build_store(
7377
},
7478
)
7579
for batch_index in range(NBATCHES):
76-
store.append(make_batch(batch_index))
77-
return store
80+
barr.append(make_batch(batch_index))
81+
return barr
7882

7983
blosc2.remove_urlpath(URLPATH)
8084
storage = blosc2.Storage(urlpath=URLPATH, mode="w", contiguous=True)
@@ -83,24 +87,24 @@ def build_store(
8387
"clevel": clevel,
8488
"use_dict": use_dict and codec in (blosc2.Codec.ZSTD, blosc2.Codec.LZ4, blosc2.Codec.LZ4HC),
8589
}
86-
with blosc2.BatchStore(
90+
with blosc2.BatchArray(
8791
storage=storage, items_per_block=ITEMS_PER_BLOCK, serializer=serializer, cparams=cparams
88-
) as store:
92+
) as barr:
8993
for batch_index in range(NBATCHES):
90-
store.append(make_batch(batch_index))
94+
barr.append(make_batch(batch_index))
9195
return None
9296

9397

94-
def measure_random_reads(store: blosc2.BatchStore) -> tuple[list[tuple[int, int, int, dict[str, int]]], list[int]]:
98+
def measure_random_reads(barr: blosc2.BatchArray) -> tuple[list[tuple[int, int, int, dict[str, int]]], list[int]]:
9599
rng = random.Random(2024)
96100
samples: list[tuple[int, int, int, dict[str, int]]] = []
97101
timings_ns: list[int] = []
98102

99103
for _ in range(N_RANDOM_READS):
100-
batch_index = rng.randrange(len(store))
104+
batch_index = rng.randrange(len(barr))
101105
item_index = rng.randrange(OBJECTS_PER_BATCH)
102106
t0 = time.perf_counter_ns()
103-
value = store[batch_index][item_index]
107+
value = barr[batch_index][item_index]
104108
timings_ns.append(time.perf_counter_ns() - t0)
105109
if value != expected_entry(batch_index, item_index):
106110
raise RuntimeError(f"Value mismatch at batch={batch_index}, item={item_index}")
@@ -117,39 +121,39 @@ def main() -> None:
117121

118122
mode_label = "in-memory" if args.in_mem else "persistent"
119123
article = "an" if args.in_mem else "a"
120-
print(f"Building {article} {mode_label} BatchStore with 1,000,000 RGB dicts and timing 1,000 random scalar reads...")
124+
print(f"Building {article} {mode_label} BatchArray with 1,000,000 RGB dicts and timing 1,000 random scalar reads...")
121125
print(f" codec: {codec.name}")
122126
print(f" clevel: {args.clevel}")
123127
print(f" serializer: {args.serializer}")
124128
print(f" use_dict: {use_dict}")
125129
print(f" in_mem: {args.in_mem}")
126130
t0 = time.perf_counter()
127-
store = build_store(
131+
barr = build_array(
128132
codec=codec, clevel=args.clevel, use_dict=use_dict, serializer=args.serializer, in_mem=args.in_mem
129133
)
130134
build_time_s = time.perf_counter() - t0
131135
if args.in_mem:
132-
assert store is not None
133-
read_store = store
136+
assert barr is not None
137+
read_array = barr
134138
else:
135-
read_store = blosc2.BatchStore(urlpath=URLPATH, mode="r", contiguous=True, items_per_block=ITEMS_PER_BLOCK)
136-
samples, timings_ns = measure_random_reads(read_store)
139+
read_array = blosc2.BatchArray(urlpath=URLPATH, mode="r", contiguous=True, items_per_block=ITEMS_PER_BLOCK)
140+
samples, timings_ns = measure_random_reads(read_array)
137141
t0 = time.perf_counter()
138142
checksum = 0
139143
nitems = 0
140-
for item in read_store.iter_items():
144+
for item in read_array.iter_items():
141145
checksum += item["blue"]
142146
nitems += 1
143147
iter_time_s = time.perf_counter() - t0
144148

145149
print()
146-
print("BatchStore benchmark")
150+
print("BatchArray benchmark")
147151
print(f" build time: {build_time_s:.3f} s")
148-
print(f" batches: {len(read_store)}")
152+
print(f" batches: {len(read_array)}")
149153
print(f" items: {TOTAL_OBJECTS}")
150-
print(f" items_per_block: {read_store.items_per_block}")
154+
print(f" items_per_block: {read_array.items_per_block}")
151155
print()
152-
print(read_store.info)
156+
print(read_array.info)
153157
print(f"Random scalar reads: {N_RANDOM_READS}")
154158
print(f" mean: {statistics.fmean(timings_ns) / 1_000:.2f} us")
155159
print(f" max: {max(timings_ns) / 1_000:.2f} us")
@@ -159,11 +163,11 @@ def main() -> None:
159163
print(f" checksum: {checksum}")
160164
print("Sample reads:")
161165
for timing_ns, batch_index, item_index, value in samples[:5]:
162-
print(f" {timing_ns / 1_000:.2f} us -> read_store[{batch_index}][{item_index}] = {value}")
166+
print(f" {timing_ns / 1_000:.2f} us -> read_array[{batch_index}][{item_index}] = {value}")
163167
if args.in_mem:
164-
print("BatchStore kept in memory")
168+
print("BatchArray kept in memory")
165169
else:
166-
print(f"BatchStore file at: {read_store.urlpath}")
170+
print(f"BatchArray file at: {read_array.urlpath}")
167171

168172

169173
if __name__ == "__main__":

0 commit comments

Comments
 (0)