Skip to content

Feat: remote deployment support and coral-remote-server in docker-compose#187

Merged
luca-heltai merged 12 commits into
mainfrom
feat/remote-deployment
May 30, 2026
Merged

Feat: remote deployment support and coral-remote-server in docker-compose#187
luca-heltai merged 12 commits into
mainfrom
feat/remote-deployment

Conversation

@pcolt

@pcolt pcolt commented May 18, 2026

Copy link
Copy Markdown
Collaborator

Overview

The Electron app always connects to localhost:2222 (SSH), localhost:8008 (visualizer), and localhost:8080 (remote server). Whether it reaches a local Docker container or a real HPC cluster is purely a network concern — the app settings never change. Opening an SSH tunnel is all it takes to switch from local to remote.

Local (containers running on your machine)

Electron app
    ├─ localhost:2222  →  Docker 2222:22  →  sshd  coral-ssh-slurm :22
    ├─ localhost:8008  →  Docker 8008:80  →  coral-visualizer       :80
    └─ localhost:8080  →  Docker 8080:80  →  coral-remote-server    :80

Remote (SSH tunnel open to the cluster)

Electron app
    ├─ localhost:2222  →  tunnel -L 2222:localhost:2222  →  node:2222  →  Docker 2222:22  →  sshd  coral-ssh-slurm :22
    ├─ localhost:8008  →  tunnel -L 8008:localhost:8008  →  node:8008  →  Docker 8008:80  →  coral-visualizer       :80
    └─ localhost:8080  →  tunnel -L 8080:localhost:8080  →  node:8080  →  Docker 8080:80  →  coral-remote-server    :80

The tunnel is opened with a single command; no configuration change in the app is needed:

ssh -L 2222:localhost:2222 -L 8008:localhost:8008 -L 8080:localhost:8080 <remote-user>@<remote-host>

The SSH public key injected into the container is configured via a gitignored .env file (SSH_PUB_KEY_PATH), so the same docker-compose.yml works on any machine without modification. A .env.example is committed as a template.

Shared data directory permissions

The containers/shared-data directory is created owned by the host user. The coral-visualizer container runs as mambauser (a non-root user baked into the micromamba base image) whose UID does not match the host user, so it falls into the "other" category and cannot write. This applies on any host — local or remote — since the UID mismatch is between the host user and the image-defined mambauser, not a property of the machine. The fix is a one-time chmod a+w containers/shared-data, documented in both docs/run-coral-docker.md and docs/remote-setup.md.

A named Docker volume would also solve the UID mismatch — Docker manages ownership internally so any container user can write regardless of UID. However, named volumes are stored in /var/lib/docker/volumes/ and are not directly accessible from the host filesystem, which means grid files and VTK outputs produced by CORAL would require docker exec or a temporary container to extract, copy, or back up. Since the shared data directory is also the working directory where the Electron app uploads graphs and downloads results via SSH, a bind mount is more practical for day-to-day use.

Summary

  • Adds docs/remote-setup.md: step-by-step guide for connecting the app to CORAL + Slurm containers on a real remote machine via SSH tunnel
  • SSH public key injection switched from Docker build secrets to a runtime volume mount (/tmp/ssh_key.pub) copied into authorized_keys by the container entrypoint — fixes ownership issues that caused publickey auth failures
  • SSH public key path made machine-agnostic via .env (SSH_PUB_KEY_PATH); .env.example committed as a template; docker-compose.yml fails fast with a clear error if the variable is unset
  • coral-remote-server added to the main docker-compose.yml so a single docker compose up starts the full stack (SSH+Slurm, visualizer, remote server)
  • Submodule pointer for coral-remote-server updated to include the data/ directory volume mount and DB_PATH env var fix
  • Remote setup guide updated with required chmod a+w containers/shared-data step and steps renumbered accordingly

Test plan

  • docker compose up -d --build starts all three containers cleanly (locally and on cluster)
  • ssh -p 2222 root@localhost 'echo hello' succeeds after containers start
  • Settings → Save & Sync connects successfully when pointing at the local container
  • Settings → Save & Sync connects successfully through the SSH tunnel to the cluster container (with coral built)
  • Visualizer reachable at http://localhost:8008 in both local and tunnelled setups
  • docker compose up without a .env file prints a clear error message
  • Visualizer can save files and use edit mode after chmod a+w containers/shared-data on the remote host

pcolt added 11 commits May 18, 2026 16:02
…ume mount

- docs: add docs/remote-setup.md with end-to-end guide for connecting the Electron app to CORAL + Slurm containers on a real remote machine over SSH
- docs: add Remote + Coral (real machine) row to README execution modes table
- chore: replace Docker build secret with volume mount for SSH authorized_keys in both Dockerfile.coral and docker-compose.yml
- chore: remove secrets section from docker-compose.yml
- docs: update CHANGELOG under [Unreleased]
- feat: add coral-remote-server service to docker-compose.yml
- chore: use directory volume mount for SQLite database to avoid Docker file-mount pitfall
- docs: update CHANGELOG under [Unreleased]
- fix: mount public key to /tmp/ssh_key.pub instead of directly as authorized_keys
- fix: copy key with correct root ownership and 600 permissions in entrypoint
- chore: add .env to .gitignore (machine-specific SSH key path)
- chore: use ${SSH_PUB_KEY_PATH} variable in docker-compose volume mount
- docs: simplify remote-setup guide to 7 steps, remove Dockerfile/docker-compose edit instructions
- docs: mention .env machine-agnostic SSH key path in changelog entry
- docs: replace -j$(nproc) with -j4 in all four build guides
- docs: replace "adjust docker-compose.yml" instruction with .env creation step in run-coral-docker.md
- docs: add port 8080 to SSH tunnel command in remote-setup.md (coral-remote-server)
- chore: use :? syntax in docker-compose so missing SSH_PUB_KEY_PATH gives a clear error
- chore: add committed .env.example template that developers copy to .env
- docs: update run-coral-docker.md and remote-setup.md to reference .env.example
@pcolt
pcolt requested a review from mfranzon May 19, 2026 10:59
@pcolt

pcolt commented May 29, 2026

Copy link
Copy Markdown
Collaborator Author

Visualizer performance analysis — remote vs local

@mfranzon

Context: coral-visualizer issue #34 documents 11 bottlenecks (B1–B11) in edit-session performance on large meshes. This PR introduces the remote deployment architecture (SSH tunnel → containers on a cluster node) described in docs/remote-setup.md. The notes below explain why the visualizer is noticeably slower in that remote setup and what can be done about it.


1. OSMesa (software rendering) — and why the hidden window matters

When the app is run locally without a container on Ubuntu, ParaView obtains a real X11 display and uses hardware OpenGL via GLX. The extra VTK window that appears is the GPU-backed render surface. When that window is hidden or minimized:

  • Some X11 + compositing-WM combinations cause glXMakeCurrent to fall back to software rendering (llvmpipe or OSMesa) for off-screen windows.
  • This is exactly the same condition as inside the Docker container, where PV_VENV=1 in the Dockerfile forces offscreen rendering through OSMesa (libosmesa6).

The benchmark slowdowns observed locally with the window hidden are therefore a faithful proxy for container performance — both paths hit the Mesa software renderer. "Window visible = faster" means GPU rendering; "window hidden = slower" means software fallback.

Fix — use EGL offscreen rendering instead of OSMesa.
EGL can access the GPU without a visible window or display server. On a cluster node with any NVIDIA/AMD GPU this replaces OSMesa's CPU path with hardware acceleration. The conda-forge ParaView package has an EGL build variant (paraview=*=*egl*). In the Dockerfile swap libosmesa6 for libegl1 plus the appropriate vendor driver, and use the EGL-enabled ParaView build. Even on a headless cluster node with a discrete GPU, EGL delivers OpenGL 4.x performance versus Mesa llvmpipe (roughly 10–100× faster render times per frame).

If there is truly no GPU on the node, Mesa's software EGL is still at least as fast as OSMesa and often faster because it uses a more modern pipeline.


2. Trame's default mode is server-side image streaming — devastating over SSH

The more fundamental problem for the remote scenario is the network protocol Trame uses by default. The current setup has no pvserver — all VTK work runs in the same Python process. Trame uses vtkRemoteView by default, which means:

Every interaction (pan / zoom / pick):
  client sends event → SSH tunnel → server
  server renders full JPEG frame → SSH tunnel → client displays

Each rendered frame for a large mesh is a high-resolution JPEG (potentially hundreds of KB). Over an SSH tunnel with real network RTT, every mouse interaction triggers a full round-trip plus an image download. This dominates interactive latency — not computation, but image streaming.

Fix — switch to client-side (local) rendering.
In this mode the VTK geometry (mesh + field data) is serialized and sent to the browser once, and the browser's WebGL engine renders it locally. Interactions (rotation, zoom) are then instantaneous with zero network round-trips. Only actual data changes (new field loaded, new timestep, edit committed) require a new transfer.

In trame-vtk this is controlled by using vtkLocalView instead of vtkRemoteView, or the local_rendering=True parameter on the view. The serialized VTK geometry (compressed binary protobuf) is typically much smaller than the equivalent stream of JPEG frames for an interactive session.

The tradeoff: the client GPU must be capable of rendering the mesh. For a 5.6 M-cell mesh this is fine on any modern laptop GPU; the geometry serialization is a one-time cost per data change.


3. pvserver + MPI — parallelizing the VTK pipeline

Currently paraview_backend.py uses paraview.simple in-process with no pvserver. The pipeline operations that dominate issue #34's benchmarks — B1 Fetch, B5 boundary extraction, B3/B4 remap — are all single-threaded.

With pvserver --server-port=11111 (or mpirun -np N pvserver --server-port=11111):

Bottleneck Effect of MPI
B5 ExtractSurface (31.6 s session begin in PR #37) VTK filter is natively MPI-parallelized; each rank processes 1/N cells; roughly linear speedup until memory bandwidth saturates
B1 servermanager.Fetch() Each rank ships 1/N of the field; serialization is parallelized
B3/B4 coordinate remap (cached in PR #35) Run on Python-side post-Fetch; MPI does not help directly; Step 3 of issue #34 (server-side mutations) is the long-term fix
Rendering With MPI+pvserver each rank renders its geometry partition; results are composited; even OSMesa benefits from N× parallelism

How to wire pvserver into the current app:

In the container entrypoint, before starting app.py:

mpirun -np 4 pvserver --server-port=11111 --force-offscreen-rendering &

Then in paraview_backend.py, before any simple.* calls:

simple.Connect("localhost", 11111)

Everything else — simple.GetActiveViewOrCreate(), Fetch(), pipeline construction — uses the connected pvserver transparently.

When pvserver runs on the same machine as the trame container, the pvserver↔app link is a Unix socket or localhost TCP (effectively zero latency). The Fetch cost is then purely serialization/deserialization, not network I/O. This is the ideal setup: pvserver and trame in the same container host, browser connects via the SSH tunnel.


4. SSH tunnel bandwidth and compression

The tunnel command in docs/remote-setup.md does not pass -C (compression). With server-side image streaming every rendered frame travels through the tunnel. -C can help for uncompressed payloads but hurts for JPEG (already compressed), so the better fix is eliminating image streaming entirely (see §2).

If staying with server-side rendering in the short term:

  • Set view.InteractiveRatio = 0.5 — renders at half resolution during mouse drag, full resolution on mouse-up. Cuts streamed image size by 4× during interaction.
  • Reduce the viewport resolution in the Electron app when in remote mode.

5. Production settings checklist

Setting Value Reason
ParaView build EGL variant (paraview=*=*egl*) GPU offscreen rendering without X11
Trame view vtkLocalView / local_rendering=True Geometry streaming — no per-frame SSH round-trip
pvserver mpirun -np N pvserver (N = core count) Parallelize pipeline and rendering
view.InteractiveRatio 0.5 4× smaller images during drag (fallback if remote rendering kept)
view.StillRenderImageReductionFactor 1 Full resolution when idle
pvserver port not exposed outside the container localhost-only; only the trame port needs the tunnel

Summary

Problem Root cause Fix
Same slowness as hidden window locally OSMesa software rendering via PV_VENV=1 EGL-enabled ParaView build + GPU in container
Slow interactive panning/zooming remotely Server-side JPEG streaming over SSH tunnel Switch to vtkLocalView (geometry streaming)
Slow session begin — B5 boundary extraction Single-threaded ExtractSurface MPI pvserver (N ranks → ~N× speedup)
Slow per-pick Fetch — B1 Single-threaded Fetch on full mesh MPI pvserver + server-side mutations (Step 3 of issue #34)
SSH tunnel latency on every interaction Image streaming architecture Geometry streaming — one-time transfer per data change

The highest-leverage single change for the remote scenario is switching to vtkLocalView (geometry streaming) — it eliminates the per-interaction SSH round-trip entirely. For raw computation speed, EGL rendering plus MPI pvserver deliver the largest gains on both local and remote deployments.

@luca-heltai
luca-heltai merged commit d281201 into main May 30, 2026
1 check passed
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.

2 participants