dimensionalOS / dimensionalOS/dimos

compressed image transport: end-to-end benchmark report (raw Image vs CompressedCodec)

Open
#2,831 13 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
4.5k
Forks
808
Avg merge
3d 5h
Merged PRs (30d)
233

Description

This is a prereq to @paul-nechifor webtransport/webrtc transport and frankly crazy we've waited this long to do this given how horrible sending raw images is. See benchmarks below and a few transport design decisions need to choose between

Option 1: CompressedCodec as a wrapper on PubSub that compresses inner transport on the wire and handles decoding on the other side with a param decode=True if the module on the other end accepts In[Image

initial implementation: https://github.com/dimensionalOS/dimos/pull/2814

Blueprint example:

unitree_go2_compressed_image = unitree_go2_basic.transports(
    {
        ("color_image", Image): CompressedCodec(
            LCMTransport("/color_image", CompressedImage), quality=80
        ),
    }
)

pretty ugly solution

Option 2: In Transport config layer, add params for compressed images and quality and pass in via cli with -o transports.*
# global_config.py 
  class GlobalConfig(...):
      compress_images: bool | None = None   # None = per-backend default
      image_quality: int = 75
      image_max_width: int | None = None

  # transport_factory.py
  _COMPRESSIBLE_TYPES = ("sensor_msgs.Image",)   # depth types excluded: jpeg can't carry them

  def _image_compression(msg_type: type | None, g: GlobalConfig) -> tuple[int, int | None] | None:
      """(quality, max_width) if this stream should default to compressed, else None."""
      if getattr(msg_type, "msg_name", None) not in _COMPRESSIBLE_TYPES:
          return None
      on = g.compress_images
      if on is None:
          # per-backend default: network wires compress, on-host stays raw
          on = g.transport in ("lcm", "zenoh", "webrtc")
      return (g.image_quality, g.image_max_width) if on else None

usage example:
compression enabled by default

dimos -o g.image_quality=50 run unitree-go2 # tune globally
DIMOS_COMPRESS_IMAGES=0 dimos run ...       # opt out globally

pretty clean, but less customizability / hidden for devs could be super hard to debug issues for new devs

Option 3: Some plum conversion thing that hides a decode inside all Image transports if it recieves CompressedImage instead

like:

@dispatch
  def _convert(msg: CompressedImage, ...) -> Image:
      return msg.decode()

also hidden and prob bad

Option 4: type-level, Annotated streams with some "compressionHint" - Compressed[Image] = [Image, compresssionHint(...)]

Requres more transport / blueprint arch changes, to autoconnect() etc. Otherwise probably cleanest

  class GO2Connection(Module):
      color_image: Out[Compressed[Image]]        # "I emit images, wire may compress"

  class Detector(Module):
      color_image: In[Image]                     # wants pixels - autoconnect inserts decode

  class WebRelay(Module):
      color_image: In[Compressed[Image]]         # wants bytes

end-to-end replay benchmark: raw Image vs Compressed Image

blueprints run for real in --replay mode (go2_short dataset, ~14Hz 720p camera), 60s per run, 2 reps per cell, headless. the ONLY difference between raw and codec is one blueprint pin:

.transports({("color_image", Image): CompressedCodec(LCMTransport("/color_image", CompressedImage), quality=75)})

harness: dimos/protocol/pubsub/benchmark/tool_replay_bench.py — adds BenchSink consumer modules (record every frame arrival; optionally burn N ms of real cv2 work per frame like a busy detector) + a host sampler (process-tree cpu/rss + loopback Mbit/s at 1Hz).

matrix:

  • unitree-go2 — full nav stack (slam, costmap, planner), 11 workers. the heavy real blueprint.
  • heavy — unitree-go2 + 4 BenchSinks each burning 20ms/frame (simulates 4 busy detector-class consumers)
  • unitree-go2-basic — 4 workers, control
  • profiles: clean (32 cores) and jetson (taskset -c 0-3, simulates robot-class onboard compute). wifi (tc netem) pending — needs sudo.

summary (medians, 2 reps pooled)

blueprint             profile  mode    frames    fps  lag@end   cpu%  rss MB  lo Mbps
heavy                 clean    raw        836   14.0     -4ms     24    7127      359
heavy                 clean    codec      855   14.3    -53ms      6    5733       47

heavy                 jetson   raw        674   11.4     -8ms     13    7433      352
heavy                 jetson   codec      855   14.3    -54ms      3    6726       44

unitree-go2           clean    raw        830   13.9    -56ms     17    6959      357
unitree-go2           clean    codec      855   14.3    -56ms      4    5610       48

unitree-go2           jetson   raw        376    6.5     -6ms      7    6926      351
unitree-go2           jetson   codec      854   14.3    -62ms      2    6502       43

unitree-go2-basic     clean    raw        197    3.3    -28ms      8    2765      332
unitree-go2-basic     clean    codec      854   14.3    -56ms      3    2571       31

delivered camera fps (replay publishes ~14Hz)

heavy               clean   raw   ██████████████████████████████ 14.0
heavy               clean   codec ██████████████████████████████ 14.3

heavy               jetson  raw   ████████████████████████ 11.4
heavy               jetson  codec ██████████████████████████████ 14.3

unitree-go2         clean   raw   ██████████████████████████████ 13.9
unitree-go2         clean   codec ██████████████████████████████ 14.3

unitree-go2         jetson  raw   ██████████████ 6.5
unitree-go2         jetson  codec ██████████████████████████████ 14.3

unitree-go2-basic   clean   raw   ███████ 3.3
unitree-go2-basic   clean   codec ██████████████████████████████ 14.3
charts

headline — full nav stack on robot-class compute (taskset 4 cores). green = codec, red = raw, dashed = published 14Hz:

delivery unitree-go2 jetson

same blueprint, clean 32-core box (delivery ties — the win is cpu/wire):

delivery unitree-go2 clean

with 4 busy detector-class consumers (20ms real work per frame each), 4 cores:

delivery heavy jetson

the 4-worker control — raw collapses even on the clean box (receiver contention):

delivery unitree-go2-basic clean

cpu over time, full nav stack clean profile (raw red vs codec green):

cpu unitree-go2 clean

tldr;

  1. on robot-class compute (jetson profile), raw loses half the camera. full nav stack pinned to 4 cores: raw delivers 6.5 of 14 fps; codec delivers all of it, steady for the whole run. with 4 busy consumers on top (heavy/jetson) raw manages 11.4 — and one raw run wedged so hard the coordinator couldnt even shut down inside 240s.

  2. LCM sheds frames instead of lagging. lag-behind-playback stays flat ~10ms in ALL cells — the raw path never delivers stale frames, it just doesnt deliver them. so "latency building over time" doesnt happen on lcm; frame LOSS is the failure mode. (on a reliable transport like zenoh it would lag instead.)

  3. codec uses LESS total cpu, not more. counterintuitive but consistent: raw 17-24% vs codec 4-6% (clean). shoveling 2.76MB messages through lcm (fragmentation, copies, drops) costs more cpu than jpeg encode+decode of the same frames. rss is ~1.3GB lower too.

  4. wire: ~350 → ~45 Mbit/s (8x). not 20x because only color_image is pinned — lidar/global_map/costmap stay raw lcm in both modes (deliberate: they're the control, and they behaved identically).

  5. unitree-go2-basic (4 workers) raw delivered only 3.3fps on a CLEAN 32-core box — worse than the full nav stack. with few workers the sink shares a contended process and 2.76MB deliveries drop at the receiving end (median inter-frame gap 141-274ms, gaps up to 2s, both reps). codec on the identical config: 14.3fps. raw lcm image delivery is fragile to receiver contention; 140KB messages are not.

repro

python -m dimos.protocol.pubsub.benchmark.tool_replay_bench \
  --blueprint unitree-go2 --mode codec --sinks 4 --work-ms 20 --duration 60 --out /tmp/bench/x
# jetson profile: prefix with `taskset -c 0-3`

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with dimos/protocol/pubsub/benchmark/tool_replay_bench.py and the initial CompressedCodec implementation in PR 2814; run the replay benchmark using the command in the issue to verify the reported results. Done requires choosing and documenting a transport-compression approach, with its scope and acceptance criteria made explicit.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
distributed-systems, networking, performance
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.