Conversation
This was referenced Oct 1, 2026
An AxisArray stream sends the same non-stream coordinate axes (channel labels, positions, ...) every message -- often more bytes than the data -- and every message unpickles into new axis objects, so consumers past a process hop can only compare them by value. The publisher now sends each such axis in full once (_AxisDef: token + axis) and thereafter as a 16-byte _AxisRef. The token is a blake2b digest of CoordinateAxis.fingerprint, computed once per axis object. The channel keeps an AxisTable of the axes it was given, copied out of the transport's memory, and substitutes them for references, so every message of a stream shares one axis object on the far side too. - Scope: the axes of a top-level AxisArray, never its stream axis (stream_dim, else "time"). Everything else pickles exactly as before. - Negotiation: a channel sends Command.ELIDE_OK after connecting (an older publisher ignores the byte); a publisher elides only while every non-local channel has said so. - Resets: a channel joining, or a channel that receives a reference it cannot resolve (it drops that message, as the stale-SHM path does, and sends Command.AXIS_RESEND), makes the publisher define every axis again. - EZMSG_DISABLE_AXIS_ELISION turns it off. - fast_replace drops the cached _wire_token with _fingerprint; unrolling the drop makes it slightly faster than the one-name loop it replaces. SHM round trip, (10, n) float32 + structured (label, x, y, z) channel axis: 256 ch 94-96 -> 86-88 us, 1024 ch 91-96 -> 87-88 us. Messages are 58% smaller. Byte payloads: unchanged within noise (perf ab, 12 rounds).
cboulay
force-pushed
the
cboulay/shm-hardening
branch
from
October 1, 2026 16:22
bb120ed to
d71704f
Compare
cboulay
force-pushed
the
cboulay/wire-elision
branch
from
October 1, 2026 16:22
62ffa8e to
c16bff6
Compare
Elision kept its own FALLBACK_STREAM_DIM ("time") for messages that do not
declare stream_dim, mirroring ezmsg-baseproc's STREAMING_DIMS by hand. Both
must agree on which axis changes every message, so move the definition to
ezmsg.util.messages.axisarray.DEFAULT_STREAM_DIM, next to stream_dim itself,
and read it from there (lazily, like elision's other axisarray imports).
Consumers such as ezmsg-baseproc can now import the same constant.
This branch was successfully deployed
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.
Stacked on #275. Draft for design review.
Why
An
AxisArraystream sends the same non-stream coordinate axes (channel labels, positions, ...) in every message, often more bytes than the data. Within a process that's free (messages share the axis objects), but across one every message serializes, copies and unpickles them again. The receiver also gets a new axis object each message, so the baseproc hash witness can only compare them by value (~540 vs ~280 ns per stateful consumer per message with a structured channel axis).Design
No
AxisArrayAPI change; it all lives in the transport (ezmsg/core/axiselision.py).AxisElision): before pickling a top-levelAxisArray, each non-streamCoordinateAxisgoes out in full the first time (_AxisDef: token + axis) and as a 16-byte_AxisRefthereafter. The token is a blake2b digest ofCoordinateAxis.fingerprint(64-bit as of CoordinateAxis.fingerprint: compute when pickling, widen to 64 bits #274), computed once per axis object. The caller's message is untouched; a bare copy with a substituted axes dict is what gets pickled.AxisTable): resolves references after unpickling, records definitions as copies that own their memory (never views into SHM; see Cross-process SHM grow kills the subscriber channel (BufferError) when a received message is still referenced #272), bounded to 256 entries.stream_dim, else"time", matching baseproc), axes without a fingerprint, and anything that isn't a top-levelAxisArray.Command.ELIDE_OKafter connecting. An older publisher's read loop ignores the byte. A publisher elides only while every non-local channel has said so, so an older channel keeps everything in full. The new commands are appended to the enum, so no existing value moves.Command.AXIS_RESENDfrom a channel that got a reference it can't resolve (it missed or evicted the definition), makes the publisher define every axis again. The channel drops that one message, as the existing stale-SHM path does.EZMSG_DISABLE_AXIS_ELISION.fast_replacealso drops the cached_wire_token. Unrolling the drop makes it slightly faster than before (609 vs 623 ns).Performance
(10, n)float32 + structured(label U8, x, y, z)channel axis, real implementation, elision on vsEZMSG_DISABLE_AXIS_ELISION:Over TCP (one serialization feeds both paths, and TCP channels negotiate and resolve the same way), loopback round trip, off → on: 256 ch 91.6–93.0 → 82.5–83.4 µs (−10%), 1024 ch 100.5–101.0 → 87.5 µs (−13%). Across a real network the saving scales with the bytes removed: ~0.46 ms per message at 1 Gb/s for 1024 channels, ~0.12 ms for 256 (estimated from sizes, not measured).
Byte payloads (
ezmsg perf abvs #275, 12 rounds): local/SHM/TCP all within ±2.4%, inside the ±3% A/A noise.Open questions
AxisArrays. Axes nested in other containers pickle in full.Tests
tests/test_axiselision.py(16): substitution rules, sharing one owned axis on the far side, relabels, unknown references, surviving the publisher-sidecopy_objon a grow, and end to end over SHM and TCP (sharing, relabel, recovery after a channel loses its table, staying off for a channel that never sendsELIDE_OK). Plus a guard thatfast_replace's unrolled drops match_DERIVED_CACHE_ATTRS. Full suite: only the 10 JSON-schema/inspect failures that also fail on the base branch.Writing these tests exposed a pre-existing grow race (a segment unlinked before a lagging channel attached it), now fixed in #275.