Skip to content

Commit 9730f69

Browse files
authored
Merge pull request #2933 from mabel-dev/draken-interations
draken improvements
2 parents 26a7ca8 + 52149c4 commit 9730f69

60 files changed

Lines changed: 556 additions & 511 deletions

Some content is hidden

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

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ b:
108108
@clear
109109
@$(PYTHON) scratch/brace.py
110110

111+
clickbench:
112+
@clear
113+
@$(PYTHON) tests/performance/clickbench/clickbench.py
114+
111115
# Aliases for backward compatibility
112116
t: test-quick
113117

opteryx/__version__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# THIS FILE IS AUTOMATICALLY UPDATED DURING THE BUILD PROCESS
22
# DO NOT EDIT THIS FILE DIRECTLY
33

4-
__build__ = 1838
4+
__build__ = 1862
55
__author__ = "@joocer"
6-
__version__ = "0.26.2-beta.1838"
6+
__version__ = "0.26.2-beta.1862"
77

88
# Store the version here so:
99
# 1) we don't load dependencies by storing it in __init__.py
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Stub module for the compiled count_distinct extension.

opteryx/compiled/aggregations/count_distinct.pyx

Lines changed: 65 additions & 229 deletions
Original file line numberDiff line numberDiff line change
@@ -5,244 +5,80 @@
55
# cython: infer_types=True
66
# cython: wraparound=False
77
# cython: boundscheck=False
8+
# cython: embedsignature=False
9+
# cython: c_string_type=bytes
10+
# cython: c_string_encoding=ascii
11+
# cython: profile=True
12+
# cython: linetrace=True
813

914
from libc.stdint cimport uint64_t
1015

1116
from opteryx.third_party.abseil.containers cimport FlatHashSet
12-
from cpython.object cimport PyObject_Hash
13-
from libc.stdint cimport int32_t, int64_t, uint8_t, uint64_t, uintptr_t
14-
from cpython.object cimport PyObject_Hash
15-
from cpython.bytes cimport PyBytes_AsString, PyBytes_Size
17+
cimport cython
18+
from libc.stdlib cimport malloc, free
19+
from libc.string cimport memset
1620

17-
from opteryx.third_party.cyan4973.xxhash cimport cy_xxhash3_64
18-
from opteryx.third_party.abseil.containers cimport FlatHashSet
21+
from opteryx.draken.interop.arrow cimport vector_from_arrow
22+
from opteryx.draken.vectors.vector cimport Vector
1923

2024
import pyarrow
2125

22-
cdef:
23-
int64_t NULL_HASH = <int64_t>0xBADF00D
24-
int64_t EMPTY_HASH = <int64_t>0xBADC0FFEE
25-
uint64_t SEED = <uint64_t>0x9e3779b97f4a7c15
26-
27-
cpdef FlatHashSet count_distinct(column, FlatHashSet seen_hashes):
28-
29-
"""Process column using type-specific handlers"""
30-
cdef object chunk
31-
32-
for chunk in column.chunks if isinstance(column, pyarrow.ChunkedArray) else [column]:
33-
dtype = chunk.type
34-
if pyarrow.types.is_string(dtype) or pyarrow.types.is_binary(dtype):
35-
process_string_chunk(chunk, seen_hashes)
36-
elif pyarrow.types.is_integer(dtype) or pyarrow.types.is_floating(dtype) or pyarrow.types.is_temporal(dtype):
37-
process_primitive_chunk(chunk, seen_hashes)
38-
elif pyarrow.types.is_list(dtype):
39-
process_list_chunk(chunk, seen_hashes)
40-
elif pyarrow.types.is_boolean(dtype):
41-
process_boolean_chunk(chunk, seen_hashes)
42-
else:
43-
process_generic_chunk(chunk, seen_hashes)
26+
27+
cdef inline FlatHashSet _count_distinct(object column, FlatHashSet seen_hashes):
28+
"""Fast distinct counter that hashes via Draken vectors when possible."""
29+
30+
cdef list chunks
31+
cdef Vector draken_vector
32+
cdef Py_ssize_t row_count = 0
33+
cdef Py_ssize_t num_chunks = 0
34+
cdef Py_ssize_t i
35+
cdef uint64_t* data_ptr = NULL
36+
cdef uint64_t[::1] hash_buffer
37+
cdef Py_ssize_t max_rows = 0
38+
39+
if seen_hashes is None:
40+
seen_hashes = FlatHashSet()
41+
42+
# Get chunks efficiently
43+
if isinstance(column, pyarrow.ChunkedArray):
44+
chunks = column.chunks
45+
num_chunks = len(chunks)
46+
else:
47+
chunks = [column]
48+
num_chunks = 1
49+
50+
# Find max chunk size
51+
for i in range(num_chunks):
52+
max_rows = max(max_rows, len(chunks[i]))
53+
54+
if max_rows > 0:
55+
data_ptr = <uint64_t*>malloc(max_rows * cython.sizeof(uint64_t))
56+
if data_ptr == NULL:
57+
raise MemoryError("Failed to allocate hash buffer")
58+
59+
try:
60+
for i in range(num_chunks):
61+
chunk = chunks[i]
62+
row_count = len(chunk)
63+
if row_count == 0:
64+
continue
65+
66+
memset(data_ptr, 0, row_count * cython.sizeof(uint64_t))
67+
hash_buffer = <uint64_t[:row_count]>data_ptr
68+
draken_vector = <Vector>vector_from_arrow(chunk)
69+
draken_vector.hash_into(hash_buffer)
70+
71+
seen_hashes.insert_many(data_ptr, row_count)
72+
finally:
73+
if data_ptr != NULL:
74+
free(data_ptr)
4475

4576
return seen_hashes
4677

4778

48-
# String Chunk Handler
49-
cdef void process_string_chunk(chunk, FlatHashSet seen_hashes):
50-
cdef:
51-
const uint8_t* validity
52-
const int32_t* offsets
53-
const char* data
54-
Py_ssize_t i, row_count, buffer_length
55-
Py_ssize_t arr_offset, offset_in_bits, offset_in_bytes, byte_index, bit_index
56-
uint64_t hash_val
57-
list buffers = chunk.buffers()
58-
Py_ssize_t str_len
59-
Py_ssize_t start, end
60-
61-
# Handle potential missing buffers
62-
validity = <uint8_t*><uintptr_t>(buffers[0].address) if len(buffers) > 0 and buffers[0] else NULL
63-
offsets = <int32_t*><uintptr_t>(buffers[1].address) if len(buffers) > 1 else NULL
64-
data = <const char*><uintptr_t>buffers[2].address if len(buffers) > 2 else NULL
65-
row_count = len(chunk)
66-
buffer_length = buffers[2].size if len(buffers) > 2 and buffers[2] else 0
67-
arr_offset = chunk.offset # Account for non-zero offset in chunk
68-
69-
# Calculate the byte and bit offset for validity
70-
offset_in_bits = arr_offset & 7
71-
offset_in_bytes = arr_offset >> 3
72-
73-
for i in range(row_count):
74-
75-
# locate validity bit for this row
76-
byte_index = offset_in_bytes + ((offset_in_bits + i) >> 3)
77-
bit_index = (offset_in_bits + i) & 7
78-
79-
# Check validity bit
80-
if validity and not (validity[byte_index] & (1 << bit_index)):
81-
hash_val = NULL_HASH
82-
else:
83-
# Calculate the position in offsets array
84-
start = offsets[arr_offset + i]
85-
end = offsets[arr_offset + i + 1]
86-
str_len = end - start
87-
88-
# Validate string length and boundaries
89-
if str_len < 0 or (start + str_len) > buffer_length:
90-
hash_val = EMPTY_HASH
91-
else:
92-
# Hash the string using xxhash3_64
93-
hash_val = <int64_t>cy_xxhash3_64(data + start, <size_t>str_len)
94-
95-
seen_hashes.just_insert(hash_val)
96-
97-
98-
# Primitive Numeric Handler (Int/Float)
99-
cdef void process_primitive_chunk(chunk, FlatHashSet seen_hashes):
100-
cdef:
101-
const uint8_t* validity
102-
const uint8_t* data
103-
Py_ssize_t i, length, item_size
104-
Py_ssize_t arr_offset, offset_in_bits, offset_in_bytes, byte_index, bit_index
105-
uint64_t hash_val
106-
list buffers = chunk.buffers()
107-
108-
validity = <uint8_t*><uintptr_t>(buffers[0].address) if buffers[0] else NULL
109-
data = <uint8_t*><uintptr_t>(buffers[1].address)
110-
length = len(chunk)
111-
item_size = chunk.type.bit_width // 8
112-
arr_offset = chunk.offset # Account for non-zero offset in chunk
113-
114-
# Calculate the byte and bit offset for validity
115-
offset_in_bits = arr_offset & 7
116-
offset_in_bytes = arr_offset >> 3
117-
118-
for i in range(length):
119-
# Correctly locate validity bit for this row
120-
byte_index = offset_in_bytes + ((offset_in_bits + i) >> 3)
121-
bit_index = (offset_in_bits + i) & 7
122-
123-
# Check validity bit, considering chunk offset
124-
if validity and not (validity[byte_index] & (1 << bit_index)):
125-
hash_val = NULL_HASH
126-
elif item_size == <uint64_t>8:
127-
# cast 8-byte values directly into hash_val
128-
hash_val = (<uint64_t*>(data + ((arr_offset + i) * 8)))[0]
129-
else:
130-
hash_val = cy_xxhash3_64(data + ((arr_offset + i) * item_size), <size_t>item_size)
131-
132-
seen_hashes.just_insert(hash_val)
133-
134-
135-
# Composite Type Handler (List)
136-
cdef void process_list_chunk(chunk, FlatHashSet seen_hashes):
137-
"""
138-
Processes a ListArray chunk by slicing the child array correctly,
139-
combining each sub-element's hash, and mixing it into `row_hashes`.
140-
"""
141-
142-
cdef:
143-
const uint8_t* validity
144-
const int32_t* offsets
145-
Py_ssize_t i, j, length, data_size
146-
Py_ssize_t start, end, sub_length
147-
Py_ssize_t arr_offset, child_offset
148-
object child_array, sublist
149-
uint64_t hash_val
150-
list buffers = chunk.buffers()
151-
uint64_t c1 = <uint64_t>0xbf58476d1ce4e5b9
152-
uint64_t c2 = <uint64_t>0x94d049bb133111eb
153-
cdef char* data_ptr
154-
155-
# Obtain addresses of validity bitmap and offsets buffer
156-
validity = <uint8_t*><uintptr_t>(buffers[0].address) if buffers[0] else NULL
157-
offsets = <int32_t*><uintptr_t>(buffers[1].address)
158-
159-
# The child array holds the sub-elements of the list
160-
child_array = chunk.values
161-
162-
# Number of "top-level" list entries in this chunk
163-
length = len(chunk)
164-
165-
# Arrow can slice a chunk, so account for chunk.offset
166-
arr_offset = chunk.offset
167-
168-
# Child array can also be offset
169-
child_offset = child_array.offset
170-
171-
for i in range(length):
172-
# Check validity for the i-th list in this chunk
173-
if validity and not (validity[i >> 3] & (1 << (i & 7))):
174-
hash_val = NULL_HASH
175-
else:
176-
# Properly compute start/end using arr_offset
177-
start = offsets[arr_offset + i]
178-
end = offsets[arr_offset + i + 1]
179-
sub_length = end - start
180-
181-
# Initialize hash with a seed
182-
hash_val = SEED
183-
184-
# Handle empty list
185-
if sub_length == 0:
186-
hash_val = EMPTY_HASH
187-
else:
188-
# Correctly slice child array by adding child_offset
189-
sublist = child_array.slice(start + child_offset, sub_length)
190-
191-
# Combine each element in the sublist
192-
for j in range(sub_length):
193-
# Convert to Python string, then to UTF-8 bytes
194-
element = sublist[j].as_py().encode("utf-8")
195-
data_ptr = PyBytes_AsString(element)
196-
data_size = PyBytes_Size(element)
197-
198-
# Combine each element's hash with a simple mix
199-
hash_val = cy_xxhash3_64(<const void*>data_ptr, <size_t>data_size) ^ hash_val
200-
201-
# Optionally apply SplitMix64 finalizer (commented out for now)
202-
hash_val = (hash_val ^ (hash_val >> 30)) * c1
203-
hash_val = (hash_val ^ (hash_val >> 27)) * c2
204-
hash_val = hash_val ^ (hash_val >> 31)
205-
206-
seen_hashes.just_insert(hash_val)
207-
208-
# Add boolean chunk handler
209-
cdef void process_boolean_chunk(chunk, FlatHashSet seen_hashes):
210-
cdef:
211-
const uint8_t* validity
212-
const uint8_t* data
213-
Py_ssize_t i, length
214-
uint64_t hash_val
215-
list buffers = chunk.buffers()
216-
217-
validity = <uint8_t*><uintptr_t>(buffers[0].address) if buffers[0] else NULL
218-
data = <uint8_t*><uintptr_t>(buffers[1].address) if buffers[1] else NULL
219-
length = len(chunk)
220-
221-
for i in range(length):
222-
if validity and not (validity[i >> 3] & (1 << (i & 7))):
223-
hash_val = NULL_HASH
224-
else:
225-
# Booleans are bit-packed - use bitwise ops to extract values
226-
hash_val = (data[i >> 3] & (1 << (i & 7))) != 0
227-
228-
seen_hashes.just_insert(hash_val)
229-
230-
231-
cdef void process_generic_chunk(chunk, FlatHashSet seen_hashes):
232-
"""Fallback handler for types without a specific handler"""
233-
cdef:
234-
const uint8_t* validity
235-
Py_ssize_t i, length
236-
uint64_t hash_val
237-
list buffers = chunk.buffers()
238-
239-
validity = <uint8_t*><uintptr_t>(buffers[0].address) if buffers[0] else NULL
240-
length = len(chunk)
241-
242-
for i in range(length):
243-
if validity and not (validity[i >> 3] & (1 << (i & 7))):
244-
hash_val = NULL_HASH
245-
else:
246-
hash_val = PyObject_Hash(chunk[i])
247-
248-
seen_hashes.just_insert(hash_val)
79+
cpdef FlatHashSet count_distinct(object column, FlatHashSet seen_hashes):
80+
return _count_distinct(column, seen_hashes)
81+
82+
83+
cpdef FlatHashSet count_distinct_draken(object column, FlatHashSet seen_hashes):
84+
return _count_distinct(column, seen_hashes)

opteryx/connectors/disk_connector.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ class DiskConnector(BaseConnector, Partitionable, PredicatePushable, LimitPushab
6464
OrsoTypes.DATE,
6565
}
6666

67+
_executor = None # Lazy initialization
68+
6769
def __init__(self, **kwargs):
6870
"""
6971
Initialize the DiskConnector, which reads datasets directly from disk.
@@ -85,7 +87,16 @@ def __init__(self, **kwargs):
8587
self.blobs_seen = 0
8688
self._stats_lock = threading.Lock()
8789
cpu_count = os.cpu_count() or 1
88-
self._max_workers = max(1, min(8, (cpu_count + 1) // 2))
90+
self._max_workers = max(1, min(cpu_count * 2, 16)) # More aggressive scaling
91+
92+
def get_executor(self):
93+
if self._executor is None:
94+
self._executor = ThreadPoolExecutor(max_workers=self._max_workers)
95+
return self._executor
96+
97+
def __del__(self):
98+
if self._executor is not None:
99+
self._executor.shutdown(wait=False)
89100

90101
def read_blob(
91102
self, *, blob_name: str, decoder, just_schema=False, projection=None, selection=None
@@ -121,6 +132,7 @@ def read_blob(
121132
If an I/O error occurs while reading the file.
122133
"""
123134
from opteryx.compiled.io.disk_reader import read_file_mmap
135+
from opteryx.compiled.io.disk_reader import unmap_memory
124136

125137
# from opteryx.compiled.io.disk_reader import unmap_memory
126138
# Read using mmap for maximum speed
@@ -153,8 +165,8 @@ def read_blob(
153165
return result
154166
finally:
155167
# CRITICAL: Clean up the memory mapping
156-
pass
157-
# unmap_memory(mmap_obj)
168+
if mmap_obj is not None:
169+
unmap_memory(mmap_obj)
158170

159171
@single_item_cache
160172
def get_list_of_blob_names(self, *, prefix: str) -> List[str]:
@@ -280,7 +292,7 @@ def process_result(num_rows, raw_size, decoded):
280292
blob_iter = iter(blob_names)
281293
pending = {}
282294

283-
with ThreadPoolExecutor(max_workers=max_workers) as executor:
295+
with self.get_executor() as executor:
284296
for _ in range(max_workers):
285297
try:
286298
blob_name = next(blob_iter)

0 commit comments

Comments
 (0)