Skip to content

feat(examples): add modal mock inference example#64

Open
pham-tuan-binh wants to merge 9 commits into
mainfrom
example/modal-mock-inference
Open

feat(examples): add modal mock inference example#64
pham-tuan-binh wants to merge 9 commits into
mainfrom
example/modal-mock-inference

Conversation

@pham-tuan-binh

Copy link
Copy Markdown
Collaborator

What

A new example under examples/python/modal-mock-inference/ that runs a Portal policy on Modal while the robot runs locally, and measures glass-to-glass latency.

The trick: the robot paints a (seq, capture_us) token into each frame as a QR code, so the timestamp travels through the real H264 encoder and jitter buffer. The cloud policy reads the QR back out and echoes the token, and the robot closes the loop on its own clock. Because the timestamp rode the video pixels, the number is true glass-to-glass, not a metadata ping.

The policy is a mock (it decodes a QR instead of running a model), so the example stays focused on the deployment pattern rather than weights or GPUs.

Layout

File Role
robot.py Local. QR camera, receives actions, reports latency / codec lag / dropped frames.
policy.py Mock LeRobot-shaped policy that decodes the QR. Also carries the Modal app, so it deploys with modal run policy.py.
portal.yaml Shared wire contract (video track, state/action schemas, fps), loaded by both sides.
README.md Step-by-step Modal setup (CLI auth, secret creation) and run guide.
pyproject.toml, .env.example Deps and credential template.

What it measures

  • glass-to-glass latency via metrics.policy.e2e_us_p50/p95 (fed the QR capture time through in_reply_to_ts_us).
  • dropped frames via gaps in the returned seq.
  • codec lag the one-way cost of the video path over the data channel, measured on the policy side as the gap between when the raw state arrived and when the fused observation fired.

Extending to a real policy

CPU-only as shipped. A real model drops in by adding gpu="A10G" to the Modal function and swapping the Policy class, which already has the LeRobot select_action call shape. The operator loop does not change.

Testing

Validated locally without a live room: both modules import, both configs load from portal.yaml, a frame from robot.make_frame decodes back through Policy.get_action, an unreadable frame returns None, the drop and codec-lag arithmetic hold, and the Modal config_path() fallback resolves. The live LiveKit connection and the real H264 pipe are exercised by following the README (needs a LiveKit Cloud project and a Modal account).

Runs a Portal operator on Modal while the robot runs locally, and measures
glass-to-glass latency by round-tripping a timestamp through the real H264
video path.

- robot.py paints a (seq, capture_us) QR code into each frame, sends it plus
  a matching state sample, and reports latency, codec lag, and dropped frames
  on its own clock.
- policy.py is a mock LeRobot-shaped policy that decodes the QR and echoes the
  token back. It also carries the Modal app, so it deploys with
  `modal run policy.py`.
- portal.yaml holds the shared wire contract, loaded by both sides.
- README.md is a step-by-step Modal setup and run guide.

The policy is CPU-only; a real model drops in by adding a GPU and swapping the
Policy class.
The policy runs on Modal, where the secret sets LIVEKIT_* as env vars, so
loading a .env there was a no-op. Drop load_dotenv (and python-dotenv from the
Modal image) from policy.py and read the environment directly. robot.py still
loads .env since it runs locally.
- robot.py: drain in-flight replies before the final tally so tail frames
  are not counted as dropped.
- policy.py: guard the codec-lag lookup with 'is not None' instead of a
  truthy check.
- policy.py: move the Modal deploy block below main() so the file reads
  top to bottom: helpers, Policy, main, deploy.
Tested end to end against a local livekit-server and hit PortalError.NoPeer:
the policy connected and immediately called set_active_operator, which RPCs
the robot, before the robot was visible in the room.

- policy.py: wait until robot_identity() is set before claiming control.
- robot.py: wait until active_operator() is set before streaming, so frames
  sent before a policy is controlling are not counted as dropped.

With both waits, 450 frames sent -> 438 round-trips, 12 dropped (6 QR-decode
misses plus 6 frames lost in the video pipe), 99% decode rate.
A ~500ms glass-to-glass with a ~50ms codec_lag showed the latency was added
after the frame arrived: the policy decoded every observation inline, so when
decode could not keep up a standing queue built and every reply carried a
stale capture time. That defeats the point of Portal.

Adopt the inference example's pattern: keep only the freshest observations in
a bounded deque and always decode the newest, dropping the backlog. Latency
now tracks the network round trip. Codec lag is measured at arrival so the
number stays meaningful.
…latency

glass2glass much larger than rtt means the media pipeline (encode, jitter
buffer, geography of the extra cloud hop) is the cost. glass2glass close to
rtt means it is just the network round trip. This tells the two apart.
The background drifted a shade every frame, so every pixel changed every
frame and H264 could not inter-frame compress: each frame was effectively a
keyframe. On a real link that balloons the bitrate, inflates codec_lag, and
congests the transport (latency spikes, burst drops). Make the background a
constant so only the QR region changes, which is also how a real camera feed
behaves.
This is an illustrative example, not a robust data channel. Revert the static
background (it starved the QR under H264's rate control and broke decoding) and
add a README note: QR over H264 is lossy so some frames drop, and latency is
mostly the distance between robot and policy. Both are called out with the fix
(byte-stream codec for pixel-exact data, co-locate for low latency).
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.

1 participant