Repository navigation
perf: numpy-native chunk path with as_numpy opt-in - #123
Merged
Merged
Conversation
pull_chunk now pulls numeric data straight into numpy memory. With as_numpy=True (per call, or as a StreamInlet constructor default) it returns a (n_samples, n_channels) array in the stream's dtype plus a float64 timestamp array, skipping list conversion entirely. The default list return is unchanged in type and values but now uses ndarray.tolist(). push_chunk routes numeric input through np.ascontiguousarray, so nested lists, flat lists, and arrays of any dtype or memory order are converted correctly. Previously a float64 array pushed to a float32 stream was reinterpreted byte-wise (silent garbage), and list input built a ctypes array element by element. String streams are unchanged apart from a tidier decode loop. Adds fmt2npdtype to pylsl.lib, an as_numpy row to the benchmark, and round-trip tests for the new paths.
dest_obj chooses whose memory the data lands in; as_numpy chooses what comes back. With both, samples is a view of dest_obj trimmed to the samples received (a single view even on the two-phase min_samples path) and timestamps is a float64 array. Without as_numpy, dest_obj behaviour is unchanged: None and a list. Docstring rewritten around that split.
…n free push_chunk for string streams now joins the encoded values into one NUL-separated buffer and derives the char* table with a cumsum, instead of constructing a ctypes c_char_p array element by element. 2.5x faster at 64 channels. free_char_p_array_memory reads the pointers liblsl allocated through a numpy view rather than indexing a ctypes pointer array. The per-string lsl_destroy_string call remains: liblsl has no bulk-free API.
lsl_pull_chunk_str mallocs a string into all data_buffer_elements slots, not just the ones it filled. The numpy-path refactor freed only num_elements, leaking the rest on every partial or empty pull. Restore freeing max_values (as before) and memset the pointer table afterwards so a stale pointer can never reach lsl_destroy_string twice.
liblsl 1.18.0.b4 adds a bulk free and NULLs unfilled chunk slots. Bind lsl_destroy_string_array optionally and use it from free_char_p_array_memory (one call instead of one per string), falling back to the per-string loop on older liblsl. NULL entries are skipped in both paths, so pylsl works with either allocation behaviour. String pull_chunk at 64 channels x 1000 samples: 10.8 ms -> 3.6 ms.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
First follow-up to the benchmark harness (#122). Makes numpy the native currency for chunked data while keeping the existing list-returning API unchanged.
StreamInlet.pull_chunkas_numpykeyword (per call) andStreamInlet(..., as_numpy=False)constructor default. Withas_numpy=True,samplesis an(n_samples, n_channels)array in the stream's dtype andtimestampsis a float64 array. Fresh arrays are allocated per call, so results never alias a reused buffer.as_numpy=False) still returns lists with identical types and values, now viandarray.tolist()instead of ctypes slicing.min_samplestwo-phase pulls concatenate correctly in both modes.dest_objandas_numpyare orthogonal:dest_objchooses whose memory the data lands in,as_numpychooses the return types. With both,samplesis a view ofdest_objtrimmed to the samples received (a single view even on themin_samplespath) andtimestampsis a float64 array. Withoutas_numpy,dest_objbehaviour is unchanged (Noneand a list).as_numpyand always return lists.StreamOutlet.push_chunknp.ascontiguousarray(x, dtype=stream_dtype), so nested lists, flat multiplexed lists, and arrays of any dtype or memory order are all handled by one path.from_buffer, producing garbage. Fortran-ordered arrays were similarly mis-sent. Both are now converted.ValueError; empty input is still a no-op.String streams
push_chunkjoins the encoded values into one NUL-separated buffer and derives thechar*table with a vectorised cumsum instead of building a ctypesc_char_parray element by element.lsl_destroy_string_arrayin a single call; older liblsl falls back to onelsl_destroy_stringper string. NULL slots are skipped either way, so both allocation behaviours are supported.Also adds
pylsl.lib.fmt2npdtypeand anas_numpyrow tobench/bench_pylsl.py.Benchmark (Apple Silicon, liblsl 1.18.0.b3, chunk=1000,
--comparevs the #122 baseline)as_numpyat 64 channels sustains ~2.4 Gvalues/s, i.e. memcpy-bound. The remaining cost in the default list path is creating Python float objects, which is inherent to returning lists. The one regression (single-channel list push, +10%) is the fixed cost ofnp.asarrayon 1000 one-element lists; I consider it acceptable given the correctness fix on the same path. ctypes call overhead is unchanged.Compatibility
pull_chunkgains a keyword;StreamInlet.__init__gains a keyword.isinstance(..., list)still pass.push_chunknow accepts inputs it previously mishandled; inputs that used to work still work.Test plan
as_numpyargument.