Skip to content

perf(sandbox): push objects a store can't sign onto a VM in parallel, and over stdin on Modal VMs - #94

Merged
earakely-scale merged 5 commits into
mainfrom
edgararakelyan/parallel-vm-push
Oct 7, 2026
Merged

earakely-scale merged 5 commits into
mainfrom
edgararakelyan/parallel-vm-push

Conversation

@earakely-scale

@earakely-scale earakely-scale commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

With a store that can't sign URLs (e.g. the local store), objects reached a VM sandbox one base64 chunk per exec, in sequence: about 0.1 MiB/s, so a 161 MB snapshot restore took 19 minutes.

  • push_object_over_exec is now the default for every VM sandbox. It streams the object a chunk per exec, each written at its offset with dd, VmSandbox._PUSHES_IN_FLIGHT (8) at once, and checks sha256 at the end.
  • push_object_over_stdin is for sandboxes whose exec takes stdin; ModalVmSandbox now uses it. Segments of at least 1 MiB are read from one open file by position, and each is sha256-checked by the exec that writes it. Other readers go as one stream.
  • An object that fits in one exec goes in one exec, on both paths.
  • Signing stores (S3) and the local provider are unchanged.

Measurements

Dev VMs, local store, every run sha256-verified.

modal_vm other remote VM provider
before 0.10 MiB/s · 0.22 s per 10 KiB file 0.12 MiB/s · 0.49 s
this PR 8.3 MiB/s (stdin) · 0.23 s 0.6 MiB/s (default path) · 0.45 s
once it implements _exec_with_stdin 12.3 MiB/s · 0.45 s

A 110 MiB image loads and runs in 15.8 s on modal_vm, and 15.1 s on the other provider over stdin.

Chaos (live VMs)

  • Killing the writers mid-push: fails in 1.3–2 s.
  • Corrupting the file mid-push: caught by sha256.
  • Terminating the VM mid-push: fails the push. On Modal, execs already running finish and verify instead.
  • For stdin implementations: race the feed against the process's end. On the other provider, drain() never returns once the VM dies.

E2B is untested (no credentials) and gets the default path.

Testing

make unit-test passes. vm_object_push_test.py runs both paths through a real shell, covering:

  • sizes and single-exec pushes;
  • segments;
  • readers that aren't plain files (e.g. gzip);
  • an object replaced or truncated mid-push;
  • garbled data, a silent exec, and a failed chunk.

🤖 Generated with Claude Code

RetriggerConfidence Score: 5/5

The PR appears safe to merge, though a failed Modal push can still leave its VM writer waiting for input.

Fix All in CursorFindings

  1. P2 Failed push leaves writer waiting ▶
Fix with agent prompt
### Issue 1
src/agent_env/providers/sandbox_providers/sandbox.py:undefined-642
When a local object is cut short during a Modal push, `pieces()` raises while `_exec_with_stdin` is feeding a running writer. The error skips `write_eof()` and `wait()`, leaving that VM process waiting for input after the push has failed. Close the input and clean up the process when feeding fails.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Unsigned-object transfers to VM sandboxes now use parallel exec chunks by default, while Modal VMs stream the data over stdin. Both paths check the bytes written, cutting the time spent transferring large objects.

  • Unsigned objects reach VM hosts through parallel exec chunks.
  • Modal VMs stream unsigned objects through exec stdin.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[VM needs an object] --> B{Store can sign a URL?}
  B -- Yes --> C[Download from signed URL]
  B -- No --> D{Modal VM?}
  D -- Yes --> E[Send segments over stdin]
  D -- No --> F[Send chunks through exec]
  E --> G[Check written bytes]
  F --> G
Loading

Reviews (4) · Last reviewed commit: "docs(sandbox): an object a store can't p..." · Reviewed by Greptile

… and over stdin on Modal VMs

An object the store couldn't sign a URL for was read into memory whole, then
appended to the VM one base64 chunk per exec, strictly in sequence. That ran
at about 0.1 MiB/s, so a 161 MB snapshot restore took 19 minutes.

- push_object_over_exec, the default for every VM sandbox, streams the
  object from the store a chunk at a time. Each chunk goes in its own exec,
  is written at its offset with `dd seek=… conv=notrunc`, and 32 are in
  flight. The chunk size is still bounded by _WFT_CHUNK_BYTES. The file is
  checked against the object's sha256 at the end.
- push_object_over_stdin sends the object as up to 8 segments at once over
  exec stdin. Each segment is checked by sha256 in the exec that writes it.
  A reader that can't seek goes as one stream. A sandbox whose exec takes
  stdin implements _exec_with_stdin and uses it; ModalVmSandbox now does.

32 MiB on dev VMs, sha256-verified:

| path                                   | modal_vm  |
| -------------------------------------- | --------- |
| before: sequential append              | 0.10 MiB/s |
| chunk per exec, 32 in flight           | 3.0 MiB/s |
| stdin, 8 segments (Modal VMs now)      | 7.5 MiB/s |

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@earakely-scale
earakely-scale requested a review from a team as a code owner October 7, 2026 06:16
Comment thread src/agent_env/providers/sandbox_providers/sandbox.py
Comment thread src/agent_env/providers/sandbox_providers/sandbox.py Outdated
Comment thread src/agent_env/providers/sandbox_providers/sandbox.py Outdated
…ned, and fail a segment the object can't fill

- When the store opens the object as a regular file, every segment reads it
  by position (os.pread) through that one descriptor. All segments then see
  one version, even if the store replaces the object during the push. Any
  other reader goes as one stream.
- A segment that reaches the object's end before its length fails the
  push. The segment's hash covers only the bytes read, so it can't catch
  that.
- A chunk is at least one block, so a provider whose command limit is under
  16 KiB fails at exec instead of pushing nothing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
data = await asyncio.to_thread(read, _STDIN_PIECE_BYTES if left is None else min(_STDIN_PIECE_BYTES, left))
if not data:
if left is not None:
raise RuntimeError(f"The object ended {left} bytes short of the segment at offset {offset} of "

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Failed push leaves writer waiting

When a local object is cut short during a Modal push, pieces() raises while _exec_with_stdin is feeding a running writer. The error skips write_eof() and wait(), leaving that VM process waiting for input after the push has failed. Close the input and clean up the process when feeding fails.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agent_env/providers/sandbox_providers/sandbox.py
Line: 595

Comment:
**Failed push leaves writer waiting**

When a local object is cut short during a Modal push, `pieces()` raises while `_exec_with_stdin` is feeding a running writer. The error skips `write_eof()` and `wait()`, leaving that VM process waiting for input after the push has failed. Close the input and clean up the process when feeding fails.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Cursor Fix in Claude Code Fix in Codex

earakely-scale and others added 3 commits October 7, 2026 03:47
…readers, and let a provider set how many execs push

- An object of one chunk goes over exec arguments in a single exec that
  writes it and prints its sha256. An object of one segment, now at least
  1 MiB, goes over stdin the same way. Each used to cost a truncate exec and
  a check exec as well, so loads of many small files paid two to three
  times as many execs.
- Only an io.BufferedReader or io.FileIO counts as a file that segments can
  read by position. A decompressing reader also names its file's
  descriptor, and reading that file would push the compressed bytes, with
  each segment's sha256 still matching.
- VmSandbox._PUSHES_IN_FLIGHT, 8 by default, is how many execs carry one
  object at once: chunks over arguments, segments over stdin. A provider
  sets its own as a class attribute.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… where exec takes stdin

A stdin exec takes more round trips than a plain one. On Modal VMs a 10 KiB
object took 0.37 s that way, against 0.23 s in one exec whose script
carries it. When the store opens the object as a file and it is smaller
than a chunk, push_object_over_stdin writes it in one such exec.

30 x 10 KiB, sha256-verified, s/file:

| path                 | modal_vm | beta_scale |
| -------------------- | -------- | ---------- |
| before (one heredoc) | 0.22     | 0.49       |
| exec arguments       | 0.23     | 0.45       |
| load_s3_file         | 0.23     | 0.45       |

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…r stdin

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@earakely-scale
earakely-scale merged commit 8cc088a into main Oct 7, 2026
13 checks passed
@earakely-scale
earakely-scale deleted the edgararakelyan/parallel-vm-push branch October 7, 2026 15:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant