Skip to content

Blackrock: read headers through an open file instead of np.fromfile and np.memmap - #1940

Open
h-mayorquin wants to merge 2 commits into
NeuralEnsemble:masterfrom
h-mayorquin:use_read_methods_on_blackrock_headers
Open

h-mayorquin wants to merge 2 commits into
NeuralEnsemble:masterfrom
h-mayorquin:use_read_methods_on_blackrock_headers

Conversation

@h-mayorquin

Copy link
Copy Markdown
Contributor

I am thinking about #1931 and this is a low-hanging fruit to: 1) improve performance (even if it does not matter at this scale) and 2) make the header parsing more remote friendly.

The idea is that np.fromfile and np.memmap take the file path and only work with a local file, and each call opens or maps the file again. Reading with seek and read on an open file works with any file object and then the header parsing does not need to change if the file is opened another way (for example with fsspec).

@cboulay cboulay left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Heberto, this is a good step and I'd be happy to see it merged. I checked it against master on all 53 Blackrock datasets in ephy_testing_data, with every nsX loaded. Headers are byte-identical, and segments, signal checksums, spikes and events match everywhere. The only change is the error text on a truncated header.

The performance win is bigger than the description suggests. The extended header is no longer an np.memmap, so the per-channel loop in _parse_header stops going through memmap.__getitem__, and parse_header gets about 4x faster (512-channel ns5: 212 → 52 ms; many_short_blocks: 4.7 → 1.2 s).

For #1931, I'd suggest one change, before or after merging: read through the handles the reader already caches (self._get_nsx_fid(nsx_nb), plus a matching _get_nev_fid()) rather than open(filename) at each site.

  • _read_nsx_dataheader is called once per data block, so a file with a few hundred pauses opens the file a few hundred times. That costs nothing locally but a round trip each over fsspec.
  • With cached handles, supporting another file opener only means changing those two getters.
  • A small helper, read_records(fid, dtype, offset, count), that raises if it gets fewer bytes than asked would also remove the repeated np.frombuffer(fid.read(n * itemsize), ...) and give a clearer error on truncated files.

We have both in our fork (a rawio utils helper and 7 call sites), and I'm happy to push them here or open a follow-up, whichever you prefer.

Comment thread neo/rawio/blackrockrawio.py Outdated
return None # v2.1 has no data headers

nsx_basic_header = np.memmap(filename, dtype=data_header_dtype, shape=1, offset=offset, mode="r")[0]
with open(filename, "rb") as fid:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one runs once per data block from _parse_nsx_data_v22_v30, so it opens the file once per block. Could it use fid = self._get_nsx_fid(nsx_nb) like the PTP parser a few hundred lines down?

Comment thread neo/rawio/blackrockrawio.py Outdated
# Get basic header structure for this spec
basic_header_dtype = NSX_BASIC_HEADER_TYPES[spec]
nsx_basic_header = np.fromfile(filename, count=1, dtype=basic_header_dtype)[0]
with open(filename, "rb") as fid:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here and at 986/1047: _get_nsx_fid(nsx_nb) is already available, and the basic and extended header could share one handle.

Comment thread neo/rawio/blackrockrawio.py Outdated
]

nev_basic_header = np.fromfile(filename, count=1, dtype=dt0)[0]
with open(filename, "rb") as fid:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the NEV, a _get_nev_fid() mirroring _get_nsx_fid would let 1006, 1663 and 1672 share a handle, and the NEV data read could use it later.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed.

Comment thread neo/rawio/blackrockrawio.py Outdated
raw_ext_header = np.memmap(filename, offset=offset_dt0, dtype=dt1, shape=shape, mode="r")
with open(filename, "rb") as fid:
fid.seek(offset_dt0)
raw_ext_header = np.frombuffer(fid.read(int(shape) * np.dtype(dt1).itemsize), dtype=dt1, count=int(shape))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: np.frombuffer on a short read gives "buffer is smaller than requested size". Checking len(data) and naming the offset and expected size would make truncated files easier to diagnose.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will address this on the common PR reader tooling where we can test it.

@h-mayorquin

Copy link
Copy Markdown
Contributor Author

It is a nice surprise that the improvements are better than I thought for a change.

I am keeping the file handles open now instead of re-opening as you requested. This is a pattern that we use already on the buffer API and more or less for the same reasons (we have discussed this before). The problem with keeping the file handles open is that it introduces possible leaks of open files so to eliminate this somehow I introduced explicit closing machinery with the dunder method __del__. I also added a __getstate__ that leaves the handles out so a parsed reader can still be pickled so we don't break this if a previous workflow was relying on this.

Regarding the small utility, I thought about it while doing this PR but blackrock is not the only reader that uses this and I was thinking about doing a small PR after this with that change. Right now, all the instances are inlined here.

@h-mayorquin

Copy link
Copy Markdown
Contributor Author

I found another error:
#1942

After you approve this and we merge it, I will take care of that one.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants