This repository contains high-performance benchmarking pipelines for EELS microscopy data acquisition. The common task is to receive high-speed STEM UDP packets, assemble detector frames on the GPU, optionally process those frames, and write or discard the output depending on whether the run is a parity, debug, or throughput test.
There are two C++ implementations of the same wire-format pipeline:
cpp/: the original NVIDIA Holoscan SDK implementation. It remains the reference for operator semantics and offline HDF5 replay.cpp_daqiri/: the daqiri implementation, pinned throughthird_party/daqiri. This is the current production-oriented RX/TX path for IGX loopback, IGX FPGA RX, and two-Spark experiments.
Both implementations use the same STEM packet geometry: 42 B Ethernet/IPv4/UDP + 64 B STEM header + 7680 B payload in a 7786 B packet.
See docs/CURRENT_PACKET_FORMAT.md for the
concise sender-facing packet and custom-header specification.
graph LR
subgraph Network Pipeline
A[Network Source] -->|"UDP Packets"| B(StemReceiverOp)
end
subgraph File Pipeline
C[HDF5 Source] -->|"Raw Data"| D(HDF5ReplayerOp)
end
B -->|"holoscan::TensorMap (GPU)"| E(PyTorchProcessorOp)
D -->|"holoscan::TensorMap (GPU)"| E
E -->|"holoscan::TensorMap (GPU)"| F(HDF5WriterOp)
F -->|"HDF5 File"| G[Disk]
The daqiri path has equivalent source, frame assembly, processor, and sink
stages, but it is configured through cpp_daqiri/configs/*.yaml and uses
daqiri DPDK engines for live network I/O. See cpp_daqiri/README.md for the
operational commands.
The daqiri pipeline is parity-capable for the DAQIRI-only gates:
- HDF5 replay accepts
uint16andfloat32[frames,H,W]datasets. - Processor controls match the Holoscan-facing config shape:
processor.noop, dark-frame subtraction, grouped BLR correction, valid-pixel masking, two-sided dynamic half-column masking with excluded edge rows, andnoop:falseframe reduction. Networkuint16and replayfloat32inputs use the same fused CUDA correction sequence. - The HDF5 writer is supported for replay, smoke, and pixel-parity runs.
- Multiple RX interfaces assemble frames independently and share one processor and serialized HDF5 sink; shared output is ordered by batch arrival.
- IGX live loopback supports non-HDS and HDS RX paths. HDS is treated as a functional parity path, not the primary 95 Gbps throughput gate.
- Production IGX RX uses
writer.noop:trueandframes_per_tensor: 16for latency and bounded buffering. Use 128-frame configs for reduced-output comparisons against Holoscan. stem_rx_igx_fpga_dual.yamlmirrors the two-interface FPGA topology and defaults to the four-source, half-tile test stream on each interface.
Validation after the latest daqiri submodule update passed the HDF5 suite, config checks, full IGX live validation, and a 30-minute non-HDS unbounded soak. The soak sent 22.4 TB at 99.632 Gbps TX, assembled 2,811,664 frames on RX, and reported zero DPDK missed, error, or out-of-buffer counters and zero sink drops/errors. A long HDS soak and a fresh end-to-end Holoscan comparison remain separate follow-up tests.
Build daqiri's base image once on the target machine, then build the STEM DAQIRI image from this repository:
git submodule update --init --recursive third_party/daqiri
cd third_party/daqiri
IMAGE_TAG=daqiri-torch:local BASE_IMAGE=torch BASE_TARGET=dpdk \
DAQIRI_ENGINE="dpdk" scripts/build-container.sh
cd ../..
docker build -f Dockerfile.daqiri \
--build-arg STEM_DAQIRI_BUILD_TX=ON \
--build-arg STEM_DAQIRI_BUILD_RX=ON \
--build-arg STEM_DAQIRI_REQUIRE_HDF5=ON \
-t stem_daqiri:parity-hdf5 .The stem_daqiri:parity-hdf5 image enables TX, RX, and mandatory HDF5
replay/writer/correction-file support.
Run repeatable offline gates:
cpp_daqiri/scripts/run_daqiri_validation.sh hdf5
cpp_daqiri/scripts/run_daqiri_validation.sh configDAQIRI HDF5 replay is finite-only and intentionally rejects
replayer.repeat:true. Holoscan can repeat/wrap HDF5 replay input, but
parity replay runs should use repeat:false on both sides.
Live loopback gates require a privileged host-network container with GPU access,
hugepages, and /tmp mounted. The DAQIRI README contains the full launch
command and the live-smoke, live-wire, hds-smoke, hds-wire,
live-writer, and live-pixel validation commands.
* Interfaces with the Holoscan Advanced Network operator (using DPDK).
* Aggregates incoming UDP packets into full 2D frames.
* Uses a custom CUDA kernel (`gather_packets`) to handle packet reordering and memory alignment, ensuring robust handling of arbitrary packet sizes.
* Emits a `holoscan::TensorMap` containing the raw frame data on the GPU.
* Acts as a file-based source for testing and benchmarking without live network hardware.
* Reads pre-recorded frames from an HDF5 file.
* Uploads frame data to GPU memory.
* Emits a `holoscan::TensorMap` identical to the receiver's output.
* Receives the GPU tensor from the receiver.
* Wraps the memory in a `torch::Tensor`.
* Performs processing in PyTorch.
* Emits the processed result as a new tensor.
* Receives the processed tensor.
* Transfers data from GPU to Host memory.
* Writes the frame to an HDF5 file for offline analysis and verification.
The Python side is organized so the reusable analysis math is separate from the exploratory study scripts:
stem_analysis/: reusable NumPy implementations of the Holoscan processor chain, detector geometry helpers, DM4 loading, ZLP/CoreLoss spectrum helpers, and stitch calibration/repair utilities.scripts/offline/run_offline_pipeline.py: canonical offline runner that reproducesPyTorchProcessorOpmathematically on HDF5 frame stacks.scripts/dark/: dark-frame creation and dark-frame inspection tools.scripts/studies/: manifest-driven NiO spectrum and stitch studies.scripts/diagnostics/: focused BLR, dark-recovery, and one-off diagnostic studies.
For details, see docs/PYTHON_ANALYSIS.md.
For dark-frame construction and runtime masking details, see
docs/DARK_FRAME_WORKFLOW.md.
This project is built on the NVIDIA holoscan SDK and holohub.
Code specific to this project was written with the help of Google Antigravity/Gemini 3.
To compile/run with PyTorch:
export LIBTORCH="/home/daquser/jrenner/libtorch"
export LD_LIBRARY_PATH="$LIBTORCH/lib:$LD_LIBRARY_PATH"
export PATH="/usr/local/cuda-12.6/bin:$LIBTORCH/bin:$PATH"
To set up the networking:
sudo /opt/nvidia/holoscan/bin/tune_system.py --set mrrs
sudo ip link set dev enP5p3s0f0np0 mtu 9000
sudo ip link set dev enP5p3s0f1np1 mtu 9000
sudo cpupower frequency-set -g performance
# Set GPU clocks
sudo nvidia-smi -pm 1
sudo nvidia-smi -lgc=$(nvidia-smi --query-gpu=clocks.max.sm --format=csv,noheader,nounits)
sudo nvidia-smi -lmc=$(nvidia-smi --query-gpu=clocks.max.mem --format=csv,noheader,nounits)
# Add IP addresses
sudo ip addr add 192.168.1.1/24 dev enP5p3s0f0np0
sudo ip addr add 192.168.2.1/24 dev enP5p3s0f1np1
sudo /opt/nvidia/holoscan/bin/tune_system.py --check all
python -m pip install --no-build-isolation -v -e .
cmake --install build --prefix /home/daquser/jrenner/libtorch