Skip to content

GPU decoder

Optional: bslz4_to_sparse.cuda needs cupy and a CUDA GPU, and Python 3. The text below is read from the docstrings in src/cuda/.

The package

GPU (CUDA) chunk2sparse and chunk2sparseCSC: the CPU classes' call API, decoding on the GPU.
Optional: importing this needs a CUDA GPU.  Two backends (`backend` says which is in use): cupy,
which compiles the kernels with NVRTC, and the driver backend, which needs only the NVIDIA driver
(the kernels are precompiled in bslz4_kernels.fatbin).  cupy is used when it imports, else the
driver backend; BSLZ4_CUDA_BACKEND=cupy or =driver chooses.  See _gpu.py for the differences.

Porting to other devices (design note): the lz4 decoder is the only part tied to NVIDIA's 32-lane
warps.  A future port, or zstd on the GPU, is expected to decompress on the CPU (without the bit
shuffle) and then run the shuffle, the matrix product and the collect on the device; those kernels
are small and need only a subgroup shuffle/ballot, shared memory and a barrier.  The CPU classes are
the test oracle for any such port.

How it differs from the CPU classes

GPU (CUDA, cupy) versions of chunk2sparse and chunk2sparseCSC, with the same call API as the CPU
classes:

    c = chunk2sparse(mask, dtype, codec)           c(chunk, cut) -> npixels, (values, indices)
    c = chunk2sparseCSC(mask, csc, dtype, codec)   c(chunk, cut) -> npixels, (values, indices),
                                                   powder
    c.multi(chunks, cut), c.from_buffer(base, offsets, lengths, cut), c.coo(chunk, cut)

Differences from the CPU classes:
  * lz4 only, 8 kB blocks (the bitshuffle default), pixels u8/u16/u32/i8/i16/i32/f32; `pipeline` is
    accepted and ignored;
  * `acc` chooses the accumulator of the matrix product: 'f64' (default: f32 weights, f64 sums as
    pyFAI), 'f32' (fast; relative error about 1e-6); a matrix from fixed_point_matrix is summed as
    'i64' (exact integer sums, returned as int64: divide by `powder_scale` for float);
  * `batch` = frames decoded together (the dense frames in GPU memory: batch x frame bytes).
    None: as many as fit in 3/4 of the device's L2 cache when that is at least 8 (the collect
    re-reads them from the cache), else 64; at most 64, and the frames and lists of a batch take
    at most 1/16 of the device memory (a guess, for small cards).  Measured on Eiger 4M u16
    frames, kernel time per frame: L40S (96 MB L2) best at 8; H200 (60 MB) and A100 (40 MB) best
    at 64, and 3 to 16 slower there.
    `group` = frames uploaded and unblocked together; None: every frame of the call, at most 256.
    The block headers of a frame are walked by one GPU thread, so the walk costs about the same
    for 1 or 256 frames.
    `unblock` = 'gpu' (that walk, a kernel) or 'cpu' (bslz4_to_sparse.block_starts on the host,
    uploaded: for a small group, or a small GPU).
    The result arrays are numpy (as the CPU classes) and are
    overwritten by the next call.  decompress(chunks) and, for chunk2sparseCSC, integrate(chunks)
    keep their result on the GPU instead: a new device array each call (see their docstrings).
  * `device`: the GPU (None: the current one), made current in the calling thread.
  * where the chunks are and where the results go, by name:
        chunks in host memory          multi, from_buffer, __call__   -> lists on the host
                                       decompress, integrate          -> arrays on the GPU
        chunks already in GPU memory   from_device                    -> lists on the host
        (GPUDirect Storage, ...)       decompress_from_device,
                                       integrate_from_device          -> arrays on the GPU
  * the matrix is the one chunk2sparseCSC takes (scipy csc / pyFAI CSC engine, or
    fixed_point_matrix); normalise_matrix and the mask folding are bslz4_to_sparse._matrix's.  Per
    batch: decompress into dense frames in GPU memory, then the matrix product (CSR, one thread
    block per bin, no atomics) and the collect of the pixels above the cut, as separate kernels
    (bslz4_split.cu).

howmany / thresholds are the CPU classes' (see _toppix.py).  On the GPU, a frame's list is written
by the device when it holds at most `cap` = nbytes / (4 + bpp) pixels (indices + values never take
more than the raw frame) and, with a howmany, by top_pixels on the device.  A frame with more than
cap pixels above the cut, when all of them were asked for (howmany None or above cap), is not
sparse: its dense frame is copied back and listed on the host (slow, and counted in `last_host`).
GPU memory per object: the dense frames (batch x frame bytes) and batch x min(howmany, cap) listed
pixels; the matrix and the mask are shared by every object of the process.

cuda.chunk2sparse

class chunk2sparse(mask, dtype=np.uint16, codec=None, pipeline=None, batch=None, howmany=None, thresholds=None, group=None, unblock='gpu', device=None)
Decode bitshuffle-lz4 chunks of (ni, nj) frames into the pixels above a cut (and not
masked).  lz4 only (a zstd chunk is refused), and no from_frames; last_host counts the frames
that fell back to the host (see the module docstring).

chunk2sparse.set_top_pixels

set_top_pixels(howmany=None, thresholds=None)
As the constructor's howmany and thresholds (_toppix): None for every pixel above the
cut.

chunk2sparse.multi

multi(chunks, cut)
Decode a batch of chunks (one frame each): npixels[nframes], (values, indices) of shape
(nframes, ni*nj).

chunk2sparse.from_buffer

from_buffer(base, offsets, lengths, cut)
As multi, for the chunks at byte offsets[i] (lengths[i]) inside the one buffer base.

chunk2sparse.call

__call__(buffer, cut)
Decode one chunk: npixels, (values, indices), and for an object with a matrix
(chunk2sparseCSC) also the powder.  values and indices are full-frame-size views of this
object's buffers, overwritten by the next call.

chunk2sparse.coo

coo(buffer, cut)
Decode one chunk to (npixels, row, col, values) as arrays of their own (safe to keep),
and for an object with a matrix also the powder.

cuda.chunk2sparseCSC

class chunk2sparseCSC(mask, csc, dtype=np.uint16, codec=None, pipeline=None, batch=None, acc=None, howmany=None, thresholds=None, group=None, unblock='gpu', device=None)
As chunk2sparse, and every frame also goes through the sparse matrix product powder = csc @
frame (mask folded into the matrix: masked pixels contribute nothing).

csc: a scipy csc / pyFAI CSC engine, used as it is (float32 weights; acc 'f64' sums by default,
or 'f32'), or the result of bslz4_to_sparse.fixed_point_matrix: the powder is then the int64
sums of its integer weights (acc 'i64'), not converted to float: float powder = powder /
powder_scale.  They are the CPU class's integers for the same matrix.  The matrix is not
changed here.

chunk2sparseCSC.set_top_pixels

set_top_pixels(howmany=None, thresholds=None)
As the constructor's howmany and thresholds (_toppix): None for every pixel above the
cut.

chunk2sparseCSC.multi

multi(chunks, cut)
Decode a batch of chunks (one frame each): npixels[nframes], (values, indices) of shape
(nframes, ni*nj).

chunk2sparseCSC.from_buffer

from_buffer(base, offsets, lengths, cut)
As multi, for the chunks at byte offsets[i] (lengths[i]) inside the one buffer base.

chunk2sparseCSC.call

__call__(buffer, cut)
Decode one chunk: npixels, (values, indices), and for an object with a matrix
(chunk2sparseCSC) also the powder.  values and indices are full-frame-size views of this
object's buffers, overwritten by the next call.

chunk2sparseCSC.coo

coo(buffer, cut)
Decode one chunk to (npixels, row, col, values) as arrays of their own (safe to keep),
and for an object with a matrix also the powder.

release_shared

release_shared()
Drop the shared matrices and masks (objects still alive keep their references).

Generated by tools/generate_docs.py from bslz4_to_sparse 0.0.22a2 (git v0.0.22-13-g610ed27, sources sha256 6929bd75ab7a).