evlib 0.9.0

Event Camera Data Processing Library
Documentation
# evlib vs RVT preprocessing benchmark

This benchmark compares evlib's RVT-preprocessing pipeline against RVT's own torch
implementation on the same task: turning the raw Gen4 1Mpx validation recording
(`moorea_2019-02-21_000_td_2257500000_2317500000`, roughly 540 million events) into the
stacked-histogram event representation used by RVT.

Run it with:

```bash
.venv/bin/python -m benchmarks.bench_rvt_pipeline --repeats 3
```

## What is compared

Four pipeline variants, all producing the identical output tensor of shape
`(1198, 20, 360, 640)` in `uint8`:

1. **evlib rust (dense scatter-add)**: `evlib.rvt.process_sequence(backend="rust")`. Reads
   the raw h5 directly (no Parquet conversion), corrects time to non-decreasing, computes
   per-window slices with `np.searchsorted`, and hands each batch of windows to a Rust
   function that scatter-adds event counts straight into a preallocated dense buffer per
   window (the analogue of torch's `tensor.put_(accumulate=True)`), clips each pixel to the
   count cutoff and casts to `uint8`. This is the fast path.
2. **evlib streaming (full)**: `evlib.rvt.process_sequence(engine="streaming")`, the default
   Polars backend. This is the real end-to-end pipeline. It includes evlib's one-time
   conversion of the raw h5 into a sorted Parquet file, then the windowed Polars build of
   the representation.
3. **evlib build only**: the same Polars build path, but reading from a pre-built Parquet
   so the one-time h5 to Parquet conversion is excluded. This isolates the per-run build
   cost from the conversion that only needs to happen once per recording.
4. **RVT torch (reference)**: RVT's genuine code, reusing the modules under `lib/RVT`
   (`H5Reader` with its numba cum-max time correction, `StackedHistogram` with
   `count_cutoff=10, fastmode=True`, and `downsample_ev_repr` nearest-exact 0.5). RVT
   reads slices straight from the raw h5 and builds dense torch tensors per window.

## Methodology

- **Same raw input** for all variants: the committed Gen4 val h5.
- **Same reference grid**: every variant windows over the committed reference
  `timestamps_us.npy` (1198 window-end timestamps), so the windowing is identical. The
  RVT reference is fed this grid directly rather than recomputing it from labels, which
  is the only adaptation made to its code.
- **Bit-identical outputs verified**: before any timing, each variant is run once and its
  output asserted `np.array_equal` to the committed reference output across all 1198
  windows. The run aborts if any variant is not bit-identical, so we never benchmark a
  broken build. All four variants pass.
- **Timing**: each variant runs in a fresh subprocess, `--repeats 3` times. The bar shows
  the median with min..max error bars. The reported time is the pipeline body wall-clock
  (excluding interpreter and import start-up, which is identical across variants).
- **Peak memory**: each subprocess reports its own peak resident set size via
  `resource.getrusage(RUSAGE_SELF).ru_maxrss`, normalised to bytes per platform (bytes on
  macOS, KiB on Linux). This captures native Polars and torch buffers that `tracemalloc`
  would miss.

## Headline numbers

Measured on this machine (macOS, CPU), medians over 3 repeats:

| pipeline | median time | peak memory |
| --- | --- | --- |
| evlib rust (dense scatter-add, raw h5) | 15.7 s | 6.34 GB |
| evlib streaming (full) | 58.5 s | 4.29 GB |
| evlib build only | 28.2 s | 3.28 GB |
| RVT torch (reference) | 23.4 s | 6.40 GB |

- **Time**: the **Rust dense scatter-add backend is about 1.5x faster than RVT's torch
  reference** (15.7 s vs 23.4 s median) on CPU, and roughly 3.7x faster than the full
  Polars streaming pipeline. It wins by skipping the h5 to Parquet conversion entirely
  (it reads the raw h5 directly) and by replacing the Polars hash group-by with a flat
  scatter-add into a preallocated dense buffer, which is the same algorithm torch uses but
  without torch's per-window tensor allocation overhead.
- **Memory**: the Rust backend also edges out RVT, using **slightly less** peak memory
  (6.34 GB vs 6.40 GB). It holds one uint32 copy of the global corrected time array
  (~2.16 GB for 540 M events, the raw h5 dtype) for the global `searchsorted`, which is the
  dominant fixed cost and matches RVT's own time-array footprint; the per-batch event slices
  and dense output buffers are small and do not grow with sequence length, so memory is
  bounded. So the Rust backend wins on both time and memory. The Polars backends use about
  1.5x less peak memory still (4.29 GB full, 3.28 GB build-only) by streaming windowed
  Parquet batches, at the cost of being slower.
- The default backend remains the Polars streaming path; the Rust scatter-add backend is
  opt-in via `backend="rust"`.

See `out/rvt_pipeline_time.png` and `out/rvt_pipeline_memory.png` for the charts, and
`out/rvt_pipeline_bench.md` for the regenerated min/median/max table.

## GPU note

evlib's build path also accepts `engine="streaming"` backed by the cudf-polars GPU
engine. That GPU path is coded but is validated separately on CUDA hardware; the numbers
above are CPU-only. The expectation is that the GPU engine shifts the time comparison in
evlib's favour, but that claim is deliberately left unmade here until it is measured on
CUDA.