Scan and compaction#

Scanner — prefix scan#

Scanner computes an inclusive or exclusive prefix scan (prefix sum) in parallel. It is the foundation for array compaction: scanning a 0/1 selection mask yields the scatter offsets of the selected elements.

#include <eagle/eagle.h>

// Prefix scan over an int array of size N
eagle::filtering::Scanner<int, aether::SumOp<int>, /*Inclusive=*/true> scanner(N);

// Fill the input array on the host
auto ref = scanner.hostRef();
// ... write the 0/1 selection flags into ref ...

// CPU scan
scanner.hostRun();

// GPU scan
scanner.upload(stream);
scanner.deviceRun(/*includeRetrieval=*/true);

// Number of selected elements after the scan
idx_t count = scanner.hostRef().numElements();

FilteringSlice — compaction#

FilteringSlice<DataT> wraps a source array of type DataT together with its own internal scanner, so compaction is one object instead of two. Construct it directly from the array to compact; fill its scanner’s selection flags (one 0/1 per element) through slice.scanner(), then run it — hostUpdate() on CPU, or upload() / deviceUpdate() / download() on GPU:

#include <eagle/eagle.h>

// "observable" here just means an array wrapper that supports
// FilteringSlice's mutation-notification hooks — any aether array type
// FilteringSlice supports works the same way.
using ArrT = eagle::util::observable::Array<idx_t>;
ArrT source(N);

eagle::filtering::FilteringSlice<ArrT> slice(source);

// Fill the 0/1 selection flags on the host
auto& flags = slice.scanner().inputs();
for (idx_t i = 0; i < source.size(); ++i)
    flags[i] = keepThisSample(i) ? 1 : 0;

// CPU: scan + scatter in one call
slice.hostUpdate();

// GPU: upload flags, scan + scatter, download the compacted result
slice.upload();
slice.deviceUpdate(/*includeRetrieval=*/true);

// slice.hostRef() now exposes only the selected elements, packed

Active-set compaction — an index map, nothing moved#

compactDevice / compactHost (eagle/filtering/Compact.h) run the same predicate → scan → scatter over raw caller buffers and produce an index map instead of a packed copy: map[0, count) holds the indices of the kept samples in ascending order, and count their number. A per-sample kernel that reads the map (thread t → sample map[t], exiting at count) then packs the live samples into full warps while every sample’s data stays in its own slot. The device face only enqueues kernels on its stream — no allocation, no synchronisation, no host read — so it can be recorded into a CUDA graph and replayed against a mask that changes between replays.

#include <eagle/eagle.h>

// terminated: n bytes on the device (set = finished)
// map: n int32, count: one uint32, scratch: compactScratchBytes(n) bytes
eagle::filtering::compactDevice(terminated, eagle::filtering::kCompactMaskIsDrop,
                                map, count, scratch, n, stream);

From Python the same step is eagle.ActiveSet (through the CUDA backend’s filtering capability group, eagle_backend_compact_device), and eagle.compaction_body() builds the loop body that compacts every few steps inside eagle.repeat_while().