@@ -160,75 +160,86 @@ def _unpack_4bit(packed: np.ndarray, n: int) -> np.ndarray:
160160
161161
162162def _pack_bitstream (flat : np .ndarray , bits : int ) -> np .ndarray :
163- """General bit-stream packing for non-byte-aligned widths (3, 6 bits)."""
163+ """Vectorized bit-stream packing for non-byte-aligned widths (3, 6 bits).
164+
165+ Strategy: every LCM(bits, 8) bits forms a self-contained block.
166+ bits=3 → LCM=24 → 8 indices × 3-bit = 3 bytes per block
167+ bits=6 → LCM=24 → 4 indices × 6-bit = 3 bytes per block
168+ All full blocks are handled with a single NumPy reduction; the
169+ tail (< block_indices elements) is handled with a tiny Python loop.
170+ """
164171 n = len (flat )
165172 n_bytes = (n * bits + 7 ) // 8
166- out = np .zeros (n_bytes , dtype = np .uint8 )
167173
168- # Vectorize using uint64 accumulators: process indices in blocks that
169- # fill an exact number of bytes.
170- # LCM(bits, 8) / bits = indices per block; LCM(bits, 8) / 8 = bytes per block
171174 lcm = _lcm (bits , 8 )
172- block_indices = lcm // bits # e.g. bits=3 → lcm=24 → 8 indices per block
173- block_bytes = lcm // 8 # e.g. bits=3 → 3 bytes per block
175+ block_indices = lcm // bits # indices per block (8 for 3-bit, 4 for 6-bit)
176+ block_bytes = lcm // 8 # bytes per block (3 for both)
174177
175- n_full_blocks = n // block_indices
178+ n_full = n // block_indices
176179 remainder = n % block_indices
177180
178- if n_full_blocks > 0 :
179- blocks = flat [: n_full_blocks * block_indices ].reshape (n_full_blocks , block_indices )
180- for b_idx in range (n_full_blocks ):
181- acc : int = 0
182- for i in range (block_indices ):
183- acc |= int (blocks [b_idx , i ]) << (i * bits )
184- base = b_idx * block_bytes
185- for j in range (block_bytes ):
186- out [base + j ] = (acc >> (j * 8 )) & 0xFF
187-
188- # Handle remainder indices
181+ # Per-position bit shifts within a block.
182+ # All accumulators fit in uint32: max value = 2^LCM(bits,8) - 1 ≤ 2^24 - 1.
183+ idx_shifts = (np .arange (block_indices , dtype = np .uint32 ) * bits )
184+ byte_shifts = (np .arange (block_bytes , dtype = np .uint32 ) * 8 )
185+
186+ out = np .zeros (n_bytes , dtype = np .uint8 )
187+
188+ if n_full > 0 :
189+ blocks = flat [: n_full * block_indices ].reshape (n_full , block_indices ).astype (np .uint32 )
190+ acc = (blocks << idx_shifts ).sum (axis = 1 , dtype = np .uint32 ) # (n_full,)
191+ bytes_2d = ((acc [:, None ] >> byte_shifts ) & 0xFF ).astype (np .uint8 ) # (n_full, block_bytes)
192+ out [: n_full * block_bytes ] = bytes_2d .ravel ()
193+
189194 if remainder :
190- acc = 0
195+ acc_r = 0
196+ base_idx = n_full * block_indices
191197 for i in range (remainder ):
192- acc |= int (flat [n_full_blocks * block_indices + i ]) << (i * bits )
193- base = n_full_blocks * block_bytes
194- remaining_bits = remainder * bits
195- remaining_bytes = (remaining_bits + 7 ) // 8
196- for j in range (remaining_bytes ):
197- out [base + j ] = (acc >> (j * 8 )) & 0xFF
198+ acc_r |= int (flat [base_idx + i ]) << (i * bits )
199+ base_out = n_full * block_bytes
200+ rem_bytes = (remainder * bits + 7 ) // 8
201+ for j in range (rem_bytes ):
202+ out [base_out + j ] = (acc_r >> (j * 8 )) & 0xFF
198203
199204 return out
200205
201206
202207def _unpack_bitstream (packed : np .ndarray , bits : int , n : int ) -> np .ndarray :
203- """General bit-stream unpacking for non-byte-aligned widths (3, 6 bits)."""
204- out = np .zeros (n , dtype = np .uint8 )
205- mask = (1 << bits ) - 1
208+ """Vectorized bit-stream unpacking for non-byte-aligned widths (3, 6 bits).
206209
210+ Inverse of _pack_bitstream: reassemble uint32 accumulators per block via
211+ NumPy reduction, then extract each index with a right-shift + mask.
212+ """
207213 lcm = _lcm (bits , 8 )
208214 block_indices = lcm // bits
209215 block_bytes = lcm // 8
210216
211- n_full_blocks = n // block_indices
217+ n_full = n // block_indices
212218 remainder = n % block_indices
219+ mask = np .uint32 ((1 << bits ) - 1 )
220+
221+ idx_shifts = (np .arange (block_indices , dtype = np .uint32 ) * bits )
222+ byte_shifts = (np .arange (block_bytes , dtype = np .uint32 ) * 8 )
223+
224+ out = np .zeros (n , dtype = np .uint8 )
213225
214- if n_full_blocks > 0 :
215- for b_idx in range (n_full_blocks ):
216- base = b_idx * block_bytes
217- acc = 0
218- for j in range (block_bytes ):
219- acc |= int (packed [base + j ]) << (j * 8 )
220- for i in range (block_indices ):
221- out [b_idx * block_indices + i ] = (acc >> (i * bits )) & mask
226+ if n_full > 0 :
227+ pb = packed [: n_full * block_bytes ].reshape (n_full , block_bytes ).astype (np .uint32 )
228+ acc = (pb << byte_shifts ).sum (axis = 1 , dtype = np .uint32 ) # (n_full,)
229+ indices_2d = ((acc [:, None ] >> idx_shifts ) & mask ).astype (np .uint8 ) # (n_full, block_indices)
230+ out [: n_full * block_indices ] = indices_2d .ravel ()
222231
223232 if remainder :
224- base = n_full_blocks * block_bytes
225- remaining_bytes = ((remainder * bits ) + 7 ) // 8
226- acc = 0
227- for j in range (remaining_bytes ):
228- if base + j < len (packed ):
229- acc |= int (packed [base + j ]) << (j * 8 )
233+ base_in = n_full * block_bytes
234+ rem_bytes = (remainder * bits + 7 ) // 8
235+ acc_r = 0
236+ for j in range (rem_bytes ):
237+ if base_in + j < len (packed ):
238+ acc_r |= int (packed [base_in + j ]) << (j * 8 )
239+ base_out = n_full * block_indices
240+ mask_int = (1 << bits ) - 1
230241 for i in range (remainder ):
231- out [n_full_blocks * block_indices + i ] = (acc >> (i * bits )) & mask
242+ out [base_out + i ] = (acc_r >> (i * bits )) & mask_int
232243
233244 return out
234245
0 commit comments