Skip to content

serve: the image encoder can run on a second PC over ssh (#1404) - #1942

Closed
TraceRecursion wants to merge 1 commit into
Niko1221:mainfrom
TraceRecursion:remote-vision
Closed

TraceRecursion wants to merge 1 commit into
Niko1221:mainfrom
TraceRecursion:remote-vision

Conversation

@TraceRecursion

@TraceRecursion TraceRecursion commented Oct 11, 2026 •

Copy link
Copy Markdown

Issue: Resolves #1404

Summary

The image encoder is already a separate process behind a line protocol (ENC <image> <out>), and it is not tied
to the server's machine, so it can run on a second PC over ssh. That takes its VRAM off the card running the
model: 1383 MiB measured, and the engine's own startup line goes 3112 -> 3641 experts (+1.00 GiB of expert
cache). 2.8 s per 1024-token picture against 15.27 s on this CPU (5.4x), and 1.2 s a picture slower than a
local GPU encoder. It does not make text faster: long-context text is the same in all three modes, because
+1 GiB against a ~47 GiB expert set is ~2% coverage. A picture-latency and VRAM trade.

What changed

  • docs/REMOTE_VISION.md (new): the page asked for in [Feature]: run the image encoder on a second PC #1404 - the exact command, the config line, what happens
    when the second PC is down, and the measurements with what they were measured on.
  • README.md: one line under "Pictures" pointing at it.
  • docs/DETAILS.md: the "remote": true paragraph in "Images (vision)", next to the other vision settings.
  • tools/remote-vision/ (new): strata_vision_remote.py (the wrapper - the protocol passes paths, not data, so
    the picture and the embeddings cross one ssh connection), strata-vision-remote.cmd (the vision.exe shim;
    Popen cannot start a .py directly), make_vocab_stub.py (an 11 MB vocabulary-only stub - strata-vision
    opens the text model with vocab_only = true, so the vision host does not need the 51 GB shard),
    remote-build.sh (builds the encoder on a Linux host), remote-vision-watchdog.sh (reaps the encoder when the
    server's port stops answering), and test-wrapper.ps1 (the check in the notes below).
  • serve/server.py: the two changes I asked about in [Feature]: run the image encoder on a second PC #1404. "remote": true keeps the encode out of the request
    FIFO: a local encoder shares this machine's GPU with the engine and must stay serialised, a remote one shares
    nothing, and holding the FIFO across the upload to a starved host freezes the API, text requests included - a
    text request sent during one encode waited 13.30 s with the turn held against 0.56 s or less with
    "remote": true (measured below). Service.ensure_vision lets the server start and serve text when the
    encoder cannot, answering 400 the image encoder is not available: ... on the first image and asking again on
    the next one. Without it, a boot-order race dropped the connection and told the client nothing at all
    (measured: status 0).
  • serve/test_server.py: 4 tests (RemoteVision). The FIFO one counts the engine's turn, so it fails if the
    remote path ever takes it (0 entries, against 1 for a local encoder), and it asserts that the client's cancel
    event reaches the encoder (serve: a client that leaves during an image encode cannot be cancelled — prepare() runs before the cancel event, and the encode holds the single request turn #1999).
  • .gitignore: tools/remote-vision/*.gguf, so the generated stub cannot be committed by accident.

Extra Notes

Rebased on the current main (477ac13c). The conflicts were with #1999 (the engine's turn is a cancellable
Service._turn(cancel) now), with the "elastic" encoder's Service._vision_up / last_image_at in the
picture path, and with the "elastic" paragraph that now sits where the vision settings are documented. The
resolution keeps all of it and only skips the turn for a remote encoder: encode_all(images, cancel) for a local
one, the same call without the turn for a remote one, and the paragraph follows the "elastic" one.

It composes with the neighbouring vision options. "elastic": true (#2189) starts the GPU encoder on the
first picture and gives its VRAM back when it sits idle; "remote": true moves the encoder off this card
entirely, so there is no local VRAM to make room for and the encode skips the turn. Upstream now points
pictures at the CPU encoder on NVIDIA cards under 24 GB (#2406, measured there at 15.27 s for a 1024-token
picture against 2.7 s here), so the two answer different points of the same trade: that one keeps the card's
VRAM and pays in seconds per picture, this one keeps the seconds and pays a second PC over ssh.

Default path. "remote": true is the opt-in and nothing without it behaves differently - including the
startup tolerance, which is gated on the same key: a LOCAL encoder that fails to start is still fatal, and still
names the cause (#1445), because that is a broken install and not an absent host. Only a remote one starts
text-only and retries.

What I checked. python -m unittest discover -s serve -t . -p "test_*.py": 852 tests, OK (15 skipped) on
this branch, rebased on 477ac13c. Real-machine checks on engine 0.1.45 (server from this branch; an RTX 5080
plus an RTX 3060 over ssh, the encoder rebuilt from this branch's tools/vision):

  • the picture path: the log has [vision-remote] encoder up: READY 2560; a text request answered in 0.87 s; one
    fresh 1600x1600 picture (1024 image tokens, none of it cached) answered in 5.23 s with the right answer, and
    the wrapper logged ...img: OK 1024 in 2.71s (10.00 MB back);
  • the encoder's host gone (same config, STRATA_VISION_SSH at a host that does not resolve): the server still
    started and served text (0.70 s), and the first image answered 400 with the image encoder is not available: the vision encoder did not start: ERR the remote vision encoder could not be started - a readable error
    instead of a dropped connection - and so did the next one (7.97 s and 7.95 s, the ssh attempt's cost);
  • the FIFO, isolated: the same fresh 1024-token picture through the LOCAL CPU encoder (about 15 s of encode, so
    the window is unambiguous) - with the turn held, a text request sent while the encode ran waited 13.30 s (the
    image request itself 20.9 s); with "remote": true, nine text requests spread over the same encode all came
    back in 0.17-0.56 s against a 0.17-0.19 s baseline (the image request 19.3 s).

Only Python and tools/ are touched, no C++/CUDA/HIP/SYCL file, so no backend rebuild is involved.

What I did not test. The server side was only exercised on Windows; the wrapper's Linux side
(remote-build.sh, the watchdog) was built and smoke-tested on the vision host but not against a Linux Strata
server. STRATA_VISION_MUX is off by default because ssh multiplexing fails on Windows (getsockname failed: Not a socket). The translated READMEs are untouched. The FIFO A/B above uses a local CPU encoder to make the
encode long enough to measure; the turn it takes is the same object a remote encoder would take. The encode
above was the first picture after a fresh encoder start (2.71 s); a warm encoder measured 1.71 s for the same
1024 tokens.

The table is from engine 0.1.40.2 (the release current on 2026-10-07/08, when it was taken): the three modes
were measured the same day on one build, which is what the deltas need. The encoder binary and its mtmd path are
unchanged since, and the config gives --vram-reserve-mib 700 explicitly, so 0.1.42's Windows auto-reserve floor
(#1376) does not apply to these numbers either. The branch itself is rebased on 0.1.45, which is what the checks
above ran on.

Left out on purpose: my own deploy helper (a PowerShell script that pushes the sources to my vision host and
rebuilds there - it hardcodes my two machines and my model paths). Happy to add a generalized version if you
want one.

Happy to split the serve/server.py behaviour and its tests into their own PR if that is easier to review;
the page and tools/remote-vision/ need each other to be usable.

@CC-David-CC

Copy link
Copy Markdown
Contributor

oh i need to study this.

The image encoder is already a separate process behind a line protocol, and it is not tied to the
server's machine, so it can run on a second PC over ssh.  That takes its VRAM off the card running
the model: 1383 MiB measured, and the expert cache goes from 3112 to 3641 experts (+1.00 GiB),
because the engine sizes it from the VRAM that is free at startup.

Measured on the reference machine (Ryzen 7 9700X, RTX 5080; vision host i7-11800H + RTX 3060
Laptop, 10 GbE; IQ3_S, 524288 context, --kv int8, engine 0.1.40.2, 2026-10-07/08).  The three
configs differ ONLY in the encoder, and every run used a distinct picture, because embeddings are
cached by the image's hash:

  one 1024x1024 picture, as the client sees it (1088 prompt tokens), median of 5
    this GPU        1.63 s      1383 MiB VRAM      3112 experts, 5.96 GiB
    another PC      2.83 s      0                  3641 experts, 6.96 GiB
    this CPU        15.27 s     0                  3641 experts, 6.96 GiB   (8 threads)

5.4x faster than the CPU encoder; 1.2 s a picture slower than a local GPU one, which is the price
of not spending the VRAM.  It does NOT make text faster: the cache does grow, but long-context text
is the same in all three modes (3.27/3.30/3.31 K prompt tok/s at 257 K tokens, 3.03/3.05/3.04 K at
463 K) - ~1 GiB against a ~47 GiB expert set is ~2% coverage.  A picture-latency and VRAM trade,
not a text-speed optimisation.

Two server-side changes, both narrow, both under "remote": true:

  - Service.ensure_vision: the server starts and serves text when the encoder cannot, and the first
    image asks for it again.  A vision host can be off at boot, and failing the whole server for
    that is the wrong trade.  Its failure is a ValueError, which the request path turns into a 400
    carrying the reason; anything else escapes prepare() to socketserver, which logs a traceback and
    drops the connection, so the client is told nothing at all (measured as "status 0", then fixed).
    Gated on the same key as the rest: a LOCAL encoder that fails to start stays fatal, with the
    cause named (Niko1221#1445) - that is a broken install, not an absent host.
  - "remote": true makes the encode skip the request FIFO.  A local encoder shares the GPU with the
    engine and must be serialised with the requests; a remote one shares nothing, and holding the
    FIFO across the upload to a starved host froze the whole API, text requests included.

tools/remote-vision/ holds the wrapper (a man in the middle for a protocol that passes paths, not
data, so the picture and the embeddings cross over one ssh connection), the vocabulary-stub builder
that spares the vision host the 51 GB text model (strata-vision opens it with vocab_only = true),
a remote build script, a watchdog that reaps the encoder when the server's port goes away, and a
test.  The server starts the wrapper through a .cmd shim, so nothing has to be packed into an exe.

The docs are the page this issue asked for (docs/REMOTE_VISION.md), the one-line pointer in the
README, and the "remote" paragraph in DETAILS.md's list of vision settings.

Opt-in and off by default: no behaviour changes without "remote": true.

694 tests pass in serve/ (11 skipped, 4 new), and the wrapper's own check passes against the
vision host: READY 2560, a 640x480 picture OK 300 in 0.77 s.
@Niko1221

Copy link
Copy Markdown
Owner

Thanks for the pull request.

We're resetting the tracker for 0.2.0-beta. There are more than 1,300 open issues and pull requests, too many for us to review properly, so we're closing everything that isn't being worked on right now. Closed here does not mean rejected.

To reopen it takes about two minutes:

  1. Rebase on main if it no longer applies, and re-run your checks on 0.2.0-beta.
  2. Start the title with its area: [Engine], [GUI], [TUI], [Benchmark], [Docs] or [Other].
  3. Fill in the pull request template: the problem and result, and the validation (what you ran, on which revision, what you saw).
  4. Comment /reopen. A check runs and either reopens the pull request or lists what is still missing.

The rules are in CONTRIBUTING.md.

@Niko1221 Niko1221 closed this Oct 19, 2026
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.

[Feature]: run the image encoder on a second PC

3 participants