diff --git a/.dockerignore b/.dockerignore
index 8440f97..4611add 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -4,7 +4,7 @@
!target/release/swactor-diag-postproc
!examples/single-gpu-inference/target/release/gpu-node
!examples/single-gpu-inference/tinygrad_worker.py
-!examples/pipeline-parallel-inference/target/release/pp-gpu-node
-!examples/pipeline-parallel-inference/target/release/pp-smoke-run
+!examples/pipeline-parallel-inference/target/release/pp-worker
+!examples/pipeline-parallel-inference/target/release/pp-orchestrator
!examples/pipeline-parallel-inference/pp_tinygrad_worker.py
!examples/pipeline-parallel-inference/pp_entrypoint.sh
diff --git a/crates/dashboard/src/html.rs b/crates/dashboard/src/html.rs
index d4edf36..ac8f89b 100644
--- a/crates/dashboard/src/html.rs
+++ b/crates/dashboard/src/html.rs
@@ -125,6 +125,7 @@ pub const ACTOR_DETAIL_HTML: &str = r##"
Actors
Distribution
Datastore
+ Fleet
@@ -569,6 +570,7 @@ pub const ACTORS_HTML: &str = r##"
Actors
Distribution
Datastore
+ Fleet
diff --git a/crates/distribution/src/iroh_driver.rs b/crates/distribution/src/iroh_driver.rs
index 554a76f..9628491 100644
--- a/crates/distribution/src/iroh_driver.rs
+++ b/crates/distribution/src/iroh_driver.rs
@@ -229,13 +229,9 @@ impl IrohDriver {
match rt.block_on(start_embedded_relay(bind_addr, config.relay_public_ip)) {
Ok((server, url)) => {
let url_str = url.to_string();
- eprintln!("Relay: embedded relay started at {url}");
(Some(server), Some(url_str), RelayMode::Custom(url.into()))
}
- Err(e) => {
- eprintln!("Relay: failed to start embedded relay: {e}, falling back");
- (None, None, config.relay_mode)
- }
+ Err(_) => (None, None, config.relay_mode),
}
}
None => (None, None, config.relay_mode),
@@ -301,33 +297,18 @@ impl IrohDriver {
Some(auth) => auth.lock().unwrap().is_allowed(&node_id),
};
if !allowed {
- eprintln!(
- "iroh driver: rejected connection from unauthorized peer {}",
- swactor::transport::hex_encode(&node_id.0[..4])
- );
conn.close(0u32.into(), b"unauthorized");
continue;
}
// Route by negotiated ALPN
let negotiated_alpn = conn.alpn();
if negotiated_alpn == ALPN {
- eprintln!(
- "iroh driver: accepted SWIM connection from {}",
- swactor::transport::hex_encode(&node_id.0[..4])
- );
swim_buf.lock().unwrap().push((node_id, conn));
} else {
- eprintln!(
- "iroh driver: accepted non-SWIM connection from {} (ALPN: {})",
- swactor::transport::hex_encode(&node_id.0[..4]),
- String::from_utf8_lossy(negotiated_alpn),
- );
other_buf.lock().unwrap().push((node_id, conn));
}
}
- Err(e) => {
- eprintln!("iroh driver: incoming connection error: {e}");
- }
+ Err(_) => {}
},
None => break, // endpoint closed
}
@@ -659,7 +640,6 @@ impl IrohDriver {
});
}
- eprintln!("iroh driver: join attempt {attempt}/{max_attempts} connecting to {}...", seed_addr.id);
diagnostics.emit_event(DiagEvent::DialStarted {
peer: seed_node_id,
attempt,
@@ -726,7 +706,6 @@ impl IrohDriver {
});
}
- eprintln!("iroh driver: join attempt {attempt}/{max_attempts} connected to {}, sending...", seed_addr.id);
let send_result: Result<(), String> = async {
let mut send = conn.open_uni().await.map_err(|e| e.to_string())?;
let tag_len = (tag.len() as u32).to_be_bytes();
@@ -740,7 +719,6 @@ impl IrohDriver {
match send_result {
Ok(()) => {
- eprintln!("iroh driver: join attempt {attempt}/{max_attempts} sent to {}", seed_addr.id);
diagnostics.emit_event(DiagEvent::MessageSent {
peer: seed_node_id,
kind: tag.to_string(),
@@ -764,10 +742,6 @@ impl IrohDriver {
return;
}
Err(e) => {
- eprintln!(
- "iroh driver: join attempt {attempt}/{max_attempts} send error to {}: {e}",
- seed_addr.id
- );
diagnostics.emit_event(DiagEvent::Error {
component: "iroh_driver".into(),
message: format!("join send error: {e}"),
@@ -778,10 +752,6 @@ impl IrohDriver {
}
}
Ok(Err(e)) => {
- eprintln!(
- "iroh driver: join attempt {attempt}/{max_attempts} connect error to {}: {e}",
- seed_addr.id
- );
let outcome = classify_dial_error_str(&e.to_string());
diagnostics.emit_event(DiagEvent::DialOutcome {
peer: seed_node_id,
@@ -792,10 +762,6 @@ impl IrohDriver {
continue;
}
Err(_) => {
- eprintln!(
- "iroh driver: join attempt {attempt}/{max_attempts} connect timeout to {}",
- seed_addr.id
- );
diagnostics.emit_event(DiagEvent::DialOutcome {
peer: seed_node_id,
attempt,
@@ -817,7 +783,6 @@ impl IrohDriver {
updated_at: Instant::now(),
});
}
- eprintln!("iroh driver: join failed after {max_attempts} attempts to {}", seed_addr.id);
});
}
@@ -832,9 +797,6 @@ impl IrohDriver {
// Collect completed background join connections
{
let mut pending = self.pending_joins.lock().unwrap();
- if !pending.is_empty() {
- eprintln!("iroh driver: collecting {} pending join connection(s)", pending.len());
- }
for result in pending.drain(..) {
self.connection_cache_tracker
.note_dial_success(result.node_id, wall_ms_now());
@@ -864,7 +826,6 @@ impl IrohDriver {
let mut failure_targets: Vec = Vec::new();
for action in actions {
if let Err(e) = self.send_action(action) {
- eprintln!("iroh driver: send error: {e}");
let target = action_target(action);
self.diagnostics.emit_event(DiagEvent::Error {
component: "iroh_driver".into(),
@@ -882,9 +843,7 @@ impl IrohDriver {
let probe_actions = self.node.report_send_failure(target);
// Best-effort send of probe actions — no recursion on failure
for action in &probe_actions {
- if let Err(e) = self.send_action(action) {
- eprintln!("iroh driver: probe send error: {e}");
- }
+ let _ = self.send_action(action);
}
}
}
@@ -1230,10 +1189,6 @@ impl IrohDriver {
self.read_streams(&conn, remote_id, &mut messages).await;
}
- if !messages.is_empty() {
- eprintln!("iroh driver: received {} message(s)", messages.len());
- }
-
(messages, new_connections)
}
@@ -1250,8 +1205,7 @@ impl IrohDriver {
Ok((tag, payload)) => {
messages.push((tag, payload, remote_id));
}
- Err(e) => {
- eprintln!("iroh driver: read error: {e}");
+ Err(_) => {
break;
}
}
@@ -1276,18 +1230,12 @@ impl IrohDriver {
match tag {
"swactor_dist::Ping" => match serde_json::from_slice::(payload) {
Ok(msg) => self.node.handle_ping(msg.from, msg.sequence, &msg.piggyback),
- Err(e) => {
- eprintln!("iroh driver: decode Ping: {e}");
- Vec::new()
- }
+ Err(_) => Vec::new(),
},
"swactor_dist::Ack" => match serde_json::from_slice::(payload) {
Ok(msg) => self.node.handle_ack(msg.from, msg.sequence, &msg.piggyback),
- Err(e) => {
- eprintln!("iroh driver: decode Ack: {e}");
- Vec::new()
- }
+ Err(_) => Vec::new(),
},
"swactor_dist::PingReq" => match serde_json::from_slice::(payload) {
@@ -1295,44 +1243,29 @@ impl IrohDriver {
self.node
.handle_ping_req(msg.from, msg.target, msg.sequence, &msg.piggyback)
}
- Err(e) => {
- eprintln!("iroh driver: decode PingReq: {e}");
- Vec::new()
- }
+ Err(_) => Vec::new(),
},
"swactor_dist::JoinRequest" => {
match serde_json::from_slice::(payload) {
Ok(msg) => self.node.handle_join_request(msg.from),
- Err(e) => {
- eprintln!("iroh driver: decode JoinRequest: {e}");
- Vec::new()
- }
+ Err(_) => Vec::new(),
}
}
"swactor_dist::JoinResponse" => {
match serde_json::from_slice::(payload) {
Ok(msg) => self.node.handle_join_response(msg.members),
- Err(e) => {
- eprintln!("iroh driver: decode JoinResponse: {e}");
- Vec::new()
- }
+ Err(_) => Vec::new(),
}
}
"swactor_dist::IndirectAck" => match serde_json::from_slice::(payload) {
Ok(msg) => self.node.handle_indirect_ack(msg.target, msg.sequence, &msg.piggyback),
- Err(e) => {
- eprintln!("iroh driver: decode IndirectAck: {e}");
- Vec::new()
- }
+ Err(_) => Vec::new(),
},
- other => {
- eprintln!("iroh driver: unknown message type: {other}");
- Vec::new()
- }
+ _ => Vec::new(),
}
}
diff --git a/examples/pipeline-parallel-inference/Cargo.lock b/examples/pipeline-parallel-inference/Cargo.lock
index 90a2d8d..0e7ba02 100644
--- a/examples/pipeline-parallel-inference/Cargo.lock
+++ b/examples/pipeline-parallel-inference/Cargo.lock
@@ -634,6 +634,7 @@ dependencies = [
"axum",
"crossbeam-queue",
"ctrlc",
+ "distribution",
"serde",
"serde_json",
"swactor",
@@ -2655,6 +2656,7 @@ dependencies = [
"base64",
"dashboard",
"distribution",
+ "futures-util",
"iroh",
"libc",
"reqwest 0.12.28",
@@ -2935,6 +2937,7 @@ dependencies = [
"bytes",
"encoding_rs",
"futures-core",
+ "futures-util",
"h2",
"http",
"http-body",
@@ -2956,12 +2959,14 @@ dependencies = [
"sync_wrapper",
"tokio",
"tokio-native-tls",
+ "tokio-util",
"tower",
"tower-http",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
+ "wasm-streams 0.4.2",
"web-sys",
]
@@ -2998,7 +3003,7 @@ dependencies = [
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
- "wasm-streams",
+ "wasm-streams 0.5.0",
"web-sys",
]
@@ -4227,6 +4232,19 @@ dependencies = [
"wasmparser",
]
+[[package]]
+name = "wasm-streams"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
[[package]]
name = "wasm-streams"
version = "0.5.0"
diff --git a/examples/pipeline-parallel-inference/Cargo.toml b/examples/pipeline-parallel-inference/Cargo.toml
index 745f0a3..e13627f 100644
--- a/examples/pipeline-parallel-inference/Cargo.toml
+++ b/examples/pipeline-parallel-inference/Cargo.toml
@@ -11,24 +11,27 @@ swactor = { path = "../..", features = ["transport", "serde", "std"] }
swactor-process = { path = "../../crates/process" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
-reqwest = { version = "0.12", features = ["json"] }
+reqwest = { version = "0.12", features = ["json", "stream"] }
+futures-util = "0.3"
tokio = { version = "1", features = ["full"] }
distribution = { path = "../../crates/distribution", features = ["iroh", "collector"] }
-# Live runtime dashboard (default features = HTTP overview/actors/topology pages,
-# served on localhost when PP_DASHBOARD is set). No tui/replay-viewer pulled in.
-dashboard = { path = "../../crates/dashboard" }
+# Live runtime dashboard (HTTP overview/actors/topology pages, served on
+# localhost when PP_DASHBOARD is set). The `live-collector` feature pulls in the
+# server-side vast.ai fold (`VastaiLivePlugin`/`fold`) so the orchestrator can
+# fold the collector's record stream into the Fleet tab. No tui/replay pulled in.
+dashboard = { path = "../../crates/dashboard", features = ["live-collector"] }
iroh = "0.98"
urlencoding = "2"
base64 = "0.22"
libc = "0.2"
[[bin]]
-name = "pp-gpu-node"
-path = "src/bin/pp_gpu_node.rs"
+name = "pp-worker"
+path = "src/bin/pp_worker.rs"
[[bin]]
-name = "pp-smoke-run"
-path = "src/bin/pp_smoke_run.rs"
+name = "pp-orchestrator"
+path = "src/bin/pp_orchestrator.rs"
[dev-dependencies]
wiremock = "0.6"
diff --git a/examples/pipeline-parallel-inference/Dockerfile b/examples/pipeline-parallel-inference/Dockerfile
index 705fda0..a736533 100644
--- a/examples/pipeline-parallel-inference/Dockerfile
+++ b/examples/pipeline-parallel-inference/Dockerfile
@@ -23,8 +23,8 @@ FROM ${BASE_IMAGE}
# Pipeline binaries (this crate's target/) + diagnostics binaries (the
# workspace-root target/). All are statically linked enough that the base
# stage's libc is all they need; the worker is pure Python.
-COPY examples/pipeline-parallel-inference/target/release/pp-gpu-node /usr/local/bin/pp-gpu-node
-COPY examples/pipeline-parallel-inference/target/release/pp-smoke-run /usr/local/bin/pp-smoke-run
+COPY examples/pipeline-parallel-inference/target/release/pp-worker /usr/local/bin/pp-worker
+COPY examples/pipeline-parallel-inference/target/release/pp-orchestrator /usr/local/bin/pp-orchestrator
COPY target/release/swactor-diag-collector /usr/local/bin/swactor-diag-collector
COPY target/release/swactor-diag-postproc /usr/local/bin/swactor-diag-postproc
COPY examples/pipeline-parallel-inference/pp_tinygrad_worker.py /usr/local/share/pp_tinygrad_worker.py
diff --git a/examples/pipeline-parallel-inference/PP_N12_DEPLOY_SESSION_REPORT.md b/examples/pipeline-parallel-inference/PP_N12_DEPLOY_SESSION_REPORT.md
deleted file mode 100644
index 79c02f2..0000000
--- a/examples/pipeline-parallel-inference/PP_N12_DEPLOY_SESSION_REPORT.md
+++ /dev/null
@@ -1,49 +0,0 @@
-# PP N=12 Deploy — Session Report
-
-## Problems encountered (trivial → blocking)
-- **GPU filter too narrow** — exact `RTX 3060` match: stage 5 hit "no offers" mid-chain. Fixed by VRAM-based selection (`PP_GPU_MIN_RAM_MB`).
-- **Offer churn** — `no_such_ask`: offers vanish between search and create.
-- **API rate-limit (HTTP 429)** — `provision_stage` retried with zero backoff + no inter-stage pacing, burning the candidate pool. Fixed: 429 backoff + `PP_LEASE_PACE_MS`.
-- **Worker crash `Code(2)` on all 12 stages** — two root causes, both invisible at first:
- - `Tensor(str)` rejected by bundled tinygrad → needed `Tensor(Path(...))`.
- - Image shipped only 4 CUDA headers; NVRTC needs the full set (`vector_types.h`). Dockerfile copied a hand-picked subset.
-- **Worker stderr swallowed** — StageActor buffers it into a `worker_exit_detail` diag event that needs a collector; none configured → error vanished.
-- **SSH auth** — `publickey` denied initially (propagation lag), then worked on 5/12 but **persistently failed on 7/12** (pp-gpu-node holds PID 1 via `exec`; vast key-injection never ran). `vastai attach`/reboot didn't fix it.
-- **`vastai execute` unusable** — "Invalid command given" (restricted command set).
-- **`--redeploy` blocked** — uses the same SSH/scp, so unusable on the 7 unreachable nodes.
-- **Slow/stalled image pulls** — cheap Korea GTX-10-series hosts; one fully stalled (0 bytes), triggering Phase-2 **autoreplace churn**.
-- **Autoreplace not disableable** in the running binary.
-- **429 on relaunch** — teardown's 12 destroys consumed the budget; absorbed by the new backoff.
-
-## Where we spent the most time
-1. **~20 min blind on the silent resolve loop** — connect-timeout SWIM noise looked like the problem but was a red herring; workers had actually crashed instantly.
-2. **Getting on a node to see the real error** — SSH flakiness, restricted `execute`, local docker repro, then manual on-node run.
-3. **Run #2 image-pull waiting** — many heartbeat ticks on slow/stalled pulls + the re-download after teardown.
-
-## Observability that was clunky / insufficient
-- Worker stderr + Python traceback never reach the container log (no collector) — had to reproduce locally and SSH a node to see `Code(2)`'s cause. *(Fixed: pp-gpu-node now prints abnormal-exit stderr.)*
-- Resolve loop emits **nothing per-stage** — orchestrator log is just SWIM gossip for up to 20 min; no per-stage worker-ready/download visibility.
-- The rich SSE diag stream (`pp_download_progress`, `worker_exit_detail`) was dead — `SWACTOR_DIAG_COLLECTOR_URL` unset.
-- vast exposes **no docker-pull %** — `status_msg` only says "Pulling from"; `disk_usage` = -1.
-- Connect-timeout logs were prominent but cosmetic — actively misleading.
-- Node-id→stage mapping had to be derived by hand from `stage_secrets`.
-
-## Where interaction with the live deployment was limited
-- SSH worked on only 5/12 nodes; no reliable shell on the rest.
-- `vastai execute` restricted; couldn't run arbitrary diagnostics via API.
-- `--redeploy` (the intended fix-forward path) depends on the same broken SSH → fix-forward on live nodes was effectively impossible; had to rebuild the image + re-lease.
-- Couldn't pause/disable autoreplace or see/intervene in image-pull progress.
-- During the "loading" (pull) phase there's no container, so no SSH at all on the node that mattered most.
-
-## Other notes
-- **Sharded fetch works** (~1.8 GB/stage, not 18 GB) — but a stale code comment claims the full GGUF is pulled, which misled diagnosis.
-- Core bugs are fixed + validated on a real GPU (stage 0 → `ready`) and in the pushed image; the remaining blocker is purely **host quality** (slow-pull hosts), not code.
-- Re-leasing fresh always re-pulls image + re-downloads model; the in-place cache advantage is lost on every teardown.
-- Highest-leverage follow-ups: (1) configure a diagnostics collector, (2) emit per-stage resolve/download progress to the orchestrator log, (3) host-throughput preflight or stalled-pull fast-replace, (4) fix the onstart so vast SSH-key injection survives (don't `exec` over it).
-
-## Fixes shipped this session
-- `vastai.rs`: 429 backoff in `provision_stage` (find + create paths) and inter-stage pacing (`PP_LEASE_PACE_MS`, default 600ms).
-- `pp_tinygrad_worker.py`: `Tensor(gguf_path)` → `Tensor(Path(gguf_path))`.
-- `Dockerfile`: copy the full CUDA include set (with a `test -f vector_types.h` build guard) instead of 4 hand-picked headers.
-- `stage_actor.rs`: mirror an abnormal worker exit's stderr tail + Python traceback to pp-gpu-node's own stderr (→ container log, collector-independent).
-- Image rebuilt + pushed (`zacheryasc/swactor-pp-gpu:latest`, digest `ca373d02…`); both bug fixes validated on a real GPU node (stage 0 reached `ready`).
diff --git a/examples/pipeline-parallel-inference/docker-compose.diag.yml b/examples/pipeline-parallel-inference/docker-compose.diag.yml
index 355df21..a4e356d 100644
--- a/examples/pipeline-parallel-inference/docker-compose.diag.yml
+++ b/examples/pipeline-parallel-inference/docker-compose.diag.yml
@@ -3,14 +3,14 @@
# The collector service runs in its own container with the bundles dir
# bind-mounted from the host so the harness can read the finalized
# tarball. Both the collector and the pp processes use the host network
-# namespace, so the stage children (spawned by `pp-smoke-run` via
+# namespace, so the stage children (spawned by `pp-orchestrator` via
# `docker-gpu-node.sh`) share localhost reachability with the
# collector — `SWACTOR_DIAG_COLLECTOR_URL=http://127.0.0.1:9080` works
# uniformly from every actor in the run.
#
# The host network choice mirrors the existing `docker-e2e.sh` shape:
-# pp-smoke-run runs on the host (orchestrator) and each
-# `pp-gpu-node` runs in its own container under `--network host`. The
+# pp-orchestrator runs on the host (orchestrator) and each
+# `pp-worker` runs in its own container under `--network host`. The
# collector container just adds one more service to that arrangement.
#
# Used by `scripts/docker-diag-e2e.sh`. Direct `docker compose up`
@@ -24,7 +24,7 @@ services:
container_name: ${PP_DIAG_COLLECTOR_NAME:-pp-diag-collector}
network_mode: host
# Override the image's default entrypoint (pp_entrypoint.sh, which runs
- # pp-gpu-node) so this container runs the collector instead. The
+ # pp-worker) so this container runs the collector instead. The
# diagnostics binaries ship in the same unified code image.
entrypoint: /usr/local/bin/swactor-diag-collector
command:
diff --git a/examples/pipeline-parallel-inference/pp_entrypoint.sh b/examples/pipeline-parallel-inference/pp_entrypoint.sh
index 28a2bfc..8ee2fbe 100644
--- a/examples/pipeline-parallel-inference/pp_entrypoint.sh
+++ b/examples/pipeline-parallel-inference/pp_entrypoint.sh
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# PID-1 supervisor for the pipeline-parallel runtime container.
#
-# The worker used to run as PID 1 (`exec pp-gpu-node`), so SSH depended on
+# The worker used to run as PID 1 (`exec pp-worker`), so SSH depended on
# vast's host-side helper winning a race against our exec, and a worker crash
# killed the whole container — no shell left to read the traceback. This script
# instead owns PID 1: it brings up sshd deterministically, runs the worker as a
@@ -41,9 +41,9 @@ echo "pp-entrypoint: sshd up on :22" >&2
# ── Worker: run as a child, tee output to a file readable over SSH ───────────
WORKER_LOG=/var/log/pp-worker.log
-echo "pp-entrypoint: launching pp-gpu-node (log -> $WORKER_LOG)" >&2
+echo "pp-entrypoint: launching pp-worker (log -> $WORKER_LOG)" >&2
set -o pipefail
-/usr/local/bin/pp-gpu-node 2>&1 | tee "$WORKER_LOG"
+/usr/local/bin/pp-worker 2>&1 | tee "$WORKER_LOG"
code=${PIPESTATUS[0]}
# ── Operator runbooks (manual, over SSH) ─────────────────────────────────────
@@ -51,23 +51,23 @@ code=${PIPESTATUS[0]}
#
# Worker hot-reload (no restart) — edit the Python in place, then SIGHUP:
# scp -P pp_tinygrad_worker.py root@:/usr/local/share/pp_tinygrad_worker.py
-# ssh 'kill -HUP $(pidof pp-gpu-node)'
-# pp-gpu-node tears down its worker and re-execs the on-disk script; the
+# ssh 'kill -HUP $(pidof pp-worker)'
+# pp-worker tears down its worker and re-execs the on-disk script; the
# swactor process (and SWIM membership) stays up across the swap.
#
# Swactor-binary swap — stop the binary, stage the new one, re-exec under
# PID 1's env (preserves STAGE/SEED_ADDR/PP_STAGE_SECRET so the node id is
# unchanged). `.new` staging avoids ETXTBSY on the mapped ELF:
-# ssh 'pkill -x pp-gpu-node' # drops to the hold below
-# scp -P pp-gpu-node root@:/usr/local/bin/pp-gpu-node.new
-# ssh 'mv -f /usr/local/bin/pp-gpu-node.new /usr/local/bin/pp-gpu-node && \
-# chmod +x /usr/local/bin/pp-gpu-node && \
+# ssh 'pkill -x pp-worker' # drops to the hold below
+# scp -P pp-worker root@:/usr/local/bin/pp-worker.new
+# ssh 'mv -f /usr/local/bin/pp-worker.new /usr/local/bin/pp-worker && \
+# chmod +x /usr/local/bin/pp-worker && \
# setsid bash -c "while IFS= read -r -d \"\" kv; do export \"\$kv\"; done \
-# < /proc/1/environ; exec /usr/local/bin/pp-gpu-node" \
+# < /proc/1/environ; exec /usr/local/bin/pp-worker" \
# >/var/log/pp-restart.log 2>&1 &2
+echo "pp-entrypoint: pp-worker exited with code $code; NOT restarting (node held for postmortem)" >&2
echo "pp-entrypoint: --- last 40 lines of $WORKER_LOG ---" >&2
tail -n 40 "$WORKER_LOG" >&2 || true
exec sleep infinity
diff --git a/examples/pipeline-parallel-inference/profiles/example.env b/examples/pipeline-parallel-inference/profiles/example.env
index 53a2f17..19a60e2 100644
--- a/examples/pipeline-parallel-inference/profiles/example.env
+++ b/examples/pipeline-parallel-inference/profiles/example.env
@@ -4,7 +4,7 @@
# across the binaries and one operator's shell. Copy it to set up a run:
#
# cp profiles/example.env profiles/local.env # untracked; put real secrets here
-# PP_PROFILE=profiles/local.env cargo run --bin pp-smoke-run -- --seed ...
+# PP_PROFILE=profiles/local.env cargo run --bin pp-orchestrator -- --seed ...
#
# When PP_PROFILE is unset, profiles/local.env is loaded automatically if it
# exists. The run scripts pick this up too (the binaries load it at startup).
@@ -25,27 +25,27 @@
#SWACTOR_IROH_RELAY_URL=https://relay.example.com
# --- Topology ---------------------------------------------------------------
-# Number of pipeline stages (>= 2). pp-smoke-run also accepts --num-stages.
+# Number of pipeline stages (>= 2). pp-orchestrator also accepts --num-stages.
#NUM_STAGES=2
# --- Compute target (vast.ai / docker) --------------------------------------
# Container image to run on each node. The run scripts already read PP_IMAGE;
-# pp-smoke-run's --image default now reads it too. Set to your registry tag.
+# pp-orchestrator's --image default now reads it too. Set to your registry tag.
#PP_IMAGE=swactor-pp-gpu:latest
-# GPU class requested when leasing on vast.ai (pp-smoke-run --gpu overrides).
+# GPU class requested when leasing on vast.ai (pp-orchestrator --gpu overrides).
#PP_GPU=RTX 3060
# --- Workload (what each stage computes) ------------------------------------
# Model identifier handed to the worker.
#MODEL=
# Python worker script + interpreter (per-stage compute). WORKER_SCRIPT is the
-# pp-gpu-node default; pp-smoke-run --worker ships a script to remote nodes.
+# pp-worker default; pp-orchestrator --worker ships a script to remote nodes.
#WORKER_SCRIPT=./pp_tinygrad_worker.py
#WORKER_CMD=python3
#PYTHON=python3
# Stub mode: skip the real worker, echo activations (fast local smoke runs).
#PP_WORKER_STUB=1
-# Inference request prompt + token budget (pp-smoke-run --prompt/--max-tokens).
+# Inference request prompt + token budget (pp-orchestrator --prompt/--max-tokens).
#MAX_TOKENS=64
# --- Timeouts (seconds; sane defaults baked in — override only if needed) ---
diff --git a/examples/pipeline-parallel-inference/profiles/local-seed.env b/examples/pipeline-parallel-inference/profiles/local-seed.env
index c159d47..20e3b2f 100644
--- a/examples/pipeline-parallel-inference/profiles/local-seed.env
+++ b/examples/pipeline-parallel-inference/profiles/local-seed.env
@@ -1,7 +1,7 @@
# Local seed-mode smoke run: everything on loopback, stub workers, no relay.
# Drives one request through a 2-stage chain without a GPU or external relay.
#
-# PP_PROFILE=profiles/local-seed.env cargo run --bin pp-smoke-run -- --seed
+# PP_PROFILE=profiles/local-seed.env cargo run --bin pp-orchestrator -- --seed
#
# (No SWACTOR_IROH_RELAY_URL: seed mode wires nodes via direct loopback addrs.)
diff --git a/examples/pipeline-parallel-inference/profiles/vastai.env b/examples/pipeline-parallel-inference/profiles/vastai.env
index 735d840..306536a 100644
--- a/examples/pipeline-parallel-inference/profiles/vastai.env
+++ b/examples/pipeline-parallel-inference/profiles/vastai.env
@@ -1,7 +1,7 @@
# WAN run on leased vast.ai GPUs. Copy to profiles/local.env and fill in the
# relay URL + image; keep that copy untracked.
#
-# PP_PROFILE=profiles/local.env cargo run --bin pp-smoke-run -- \
+# PP_PROFILE=profiles/local.env cargo run --bin pp-orchestrator -- \
# --vastai --api-key "$VASTAI_API_KEY"
#
# All nodes home onto one operator-controlled relay so they can hole-punch /
@@ -13,6 +13,10 @@ SWACTOR_IROH_RELAY_URL=https://relay.example.com
NUM_STAGES=2
PP_IMAGE=swactor-pp-gpu:latest
-PP_GPU=RTX 3060
-MODEL=
+# GPU filters are optional. Leave both unset to let any verified 1-GPU offer
+# qualify (a 1B model fits anywhere). Set PP_GPU="RTX 3060" to pin a model, and/or
+# PP_GPU_MIN_RAM_MB=8000 to require a VRAM floor.
+# llama3.2:1b -> HF Llama-3.2-1B-Instruct-Q6_K.gguf (16 blocks, sharded across
+# the N stages by compute_layer_range; validated key baked into the image).
+MODEL=llama3.2:1b
MAX_TOKENS=64
diff --git a/examples/pipeline-parallel-inference/scripts/demo-fleet.sh b/examples/pipeline-parallel-inference/scripts/demo-fleet.sh
index 4b79e2f..43cbd84 100755
--- a/examples/pipeline-parallel-inference/scripts/demo-fleet.sh
+++ b/examples/pipeline-parallel-inference/scripts/demo-fleet.sh
@@ -1,17 +1,23 @@
#!/usr/bin/env bash
# demo-fleet.sh — one-command local mock of a vast.ai fleet, watchable live.
#
-# Brings up, from a single command, a self-contained demo that streams the
-# REAL host metrics each container measures (no synthetic data) into a live
-# browser dashboard:
+# Brings up, from a single command, a self-contained demo that mirrors the
+# production topology: a diagnostics collector running "off-box" (in prod, a
+# VPS) and the orchestrator running locally and hosting the FULL swactor
+# dashboard. The orchestrator's dashboard shows its own live swactor process
+# info (overview / actors / topology / distribution / netmap) and a Fleet tab
+# that pulls the vast.ai + host metrics remotely from the collector:
#
-# - swactor-diag-collector on the HOST (HTTP 9080 + UDP 9081), serving the
-# live fleet board at http://127.0.0.1:9080/dashboard
-# - pp-smoke-run on the HOST in --seed mode, spawning N pp-gpu-node
-# containers (one per stage) via docker-gpu-node.sh, each on --network host
+# - swactor-diag-collector on the HOST (HTTP 9080 + UDP 9081) — the "VPS"
+# sink. Each stage's in-VM monitor ships REAL host_sample + log records
+# (no synthetic data) here; the orchestrator pushes its distribution
+# snapshot here too. Its own fleet board stays at http://127.0.0.1:9080/dashboard
+# - pp-orchestrator on the HOST in --seed mode (PP_DASHBOARD on), spawning N
+# pp-worker containers (one per stage) via docker-gpu-node.sh, each on
+# --network host, and serving the full dashboard at http://127.0.0.1:9095/
# - PP_HOLD=1 keeps the cluster up after the first drive, so every stage's
-# in-VM monitor keeps shipping host_sample + log records (~every 5s) and
-# the dashboard animates in real time.
+# in-VM monitor keeps shipping records (~every 5s) and the dashboard
+# animates in real time.
#
# Ctrl+C (or any exit) tears everything down: stage containers, collector,
# orchestrator, and all temp files.
@@ -31,6 +37,7 @@
# PP_MAX_TOKENS decode token cap (default: 4)
# PP_BIND_HOST collector bind host (default: 127.0.0.1)
# PP_PORT collector HTTP port (default: 9080)
+# PP_DASHBOARD_PORT orchestrator dashboard HTTP port (default: 9095)
# PP_NO_OPEN if set, don't try to open the dashboard in a browser
# PP_DIAG_NETWORK docker network for stages (default: host)
set -euo pipefail
@@ -43,9 +50,13 @@ MAX_TOKENS="${PP_MAX_TOKENS:-4}"
BIND_HOST="${PP_BIND_HOST:-127.0.0.1}"
PORT="${PP_PORT:-9080}"
UDP_PORT=$((PORT + 1))
+DASH_PORT="${PP_DASHBOARD_PORT:-9095}"
CONTAINER_PREFIX="demo-fleet-stage"
RUN_ID="demo-fleet-$(date +%s)"
-DASH_URL="http://${BIND_HOST}:${PORT}/dashboard"
+# The full swactor dashboard is served by the orchestrator at "/"; the collector
+# keeps its own standalone fleet board at :PORT/dashboard.
+DASH_URL="http://${BIND_HOST}:${DASH_PORT}/"
+COLLECTOR_URL="http://${BIND_HOST}:${PORT}"
if ! [[ "$NUM_STAGES" =~ ^[0-9]+$ ]] || [ "$NUM_STAGES" -lt 2 ]; then
echo "demo-fleet: N must be an integer >= 2 (seed mode needs >=2 stages), got '$NUM_STAGES'" >&2
@@ -57,26 +68,36 @@ fi
if ! docker info >/dev/null 2>&1; then
echo "demo-fleet: docker daemon unreachable" >&2; exit 2
fi
+# Fail loudly on a clash for the orchestrator dashboard port. Its HTTP server is
+# spawned on the driver's tokio runtime and `.expect()`s its bind; a collision
+# panics that task silently and the run carries on with no dashboard. Catch it
+# here so the user can pick a free one.
+if (exec 3<>"/dev/tcp/${BIND_HOST}/${DASH_PORT}") 2>/dev/null; then
+ exec 3>&- 3<&-
+ echo "demo-fleet: dashboard port ${DASH_PORT} is already in use." \
+ "Pick a free one: PP_DASHBOARD_PORT=9096 $0 ${NUM_STAGES}" >&2
+ exit 2
+fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CRATE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
WORKSPACE_DIR="$(cd "$CRATE_DIR/../.." && pwd)"
-SMOKE_RUN_BIN="$CRATE_DIR/target/release/pp-smoke-run"
-GPU_NODE_BIN="$CRATE_DIR/target/release/pp-gpu-node"
+ORCHESTRATOR_BIN="$CRATE_DIR/target/release/pp-orchestrator"
+WORKER_BIN="$CRATE_DIR/target/release/pp-worker"
WORKER_PY="$CRATE_DIR/pp_tinygrad_worker.py"
COLLECTOR_BIN="$WORKSPACE_DIR/target/release/swactor-diag-collector"
# ── Step 1: build release artifacts ───────────────────────────────────────
if [ -z "${PP_SKIP_BUILD:-}" ]; then
- echo "demo-fleet: cargo build pp-smoke-run + pp-gpu-node (release)"
+ echo "demo-fleet: cargo build pp-orchestrator + pp-worker (release)"
cargo build --manifest-path "$CRATE_DIR/Cargo.toml" --release \
- --bin pp-gpu-node --bin pp-smoke-run
+ --bin pp-worker --bin pp-orchestrator
echo "demo-fleet: cargo build swactor-diag-collector (release, --features collector)"
cargo build --manifest-path "$WORKSPACE_DIR/Cargo.toml" --release \
-p distribution --features collector --bin swactor-diag-collector
fi
-for f in "$SMOKE_RUN_BIN" "$GPU_NODE_BIN" "$WORKER_PY" "$COLLECTOR_BIN"; do
+for f in "$ORCHESTRATOR_BIN" "$WORKER_BIN" "$WORKER_PY" "$COLLECTOR_BIN"; do
[ -f "$f" ] || { echo "demo-fleet: missing $f (run without PP_SKIP_BUILD)" >&2; exit 1; }
done
@@ -129,7 +150,7 @@ cleanup() {
trap cleanup EXIT
trap 'exit 130' INT TERM
-# ── Step 3: collector on the host (serves the dashboard) ───────────────────
+# ── Step 3: collector on the host (the off-box "VPS" metrics sink) ─────────
echo "demo-fleet: starting collector on ${BIND_HOST}:${PORT} (root=$COLLECTOR_ROOT)"
"$COLLECTOR_BIN" --bind "${BIND_HOST}:${PORT}" --root "$COLLECTOR_ROOT" \
--udp "${BIND_HOST}:${UDP_PORT}" >"$COLLECTOR_LOG" 2>&1 &
@@ -148,38 +169,31 @@ until (echo > "/dev/tcp/${BIND_HOST}/${PORT}") >/dev/null 2>&1; do
done
echo "demo-fleet: collector ready"
-# ── Step 4: announce + open the dashboard ──────────────────────────────────
-echo
-echo " ┌─────────────────────────────────────────────────────────────┐"
-echo " │ Live fleet dashboard: $DASH_URL"
-echo " └─────────────────────────────────────────────────────────────┘"
-echo
-if [ -z "${PP_NO_OPEN:-}" ]; then
- if command -v xdg-open >/dev/null 2>&1; then (xdg-open "$DASH_URL" >/dev/null 2>&1 &) || true
- elif command -v open >/dev/null 2>&1; then (open "$DASH_URL" >/dev/null 2>&1 &) || true
- fi
-fi
-
-# ── Step 5: orchestrator (holds the cluster open, real metrics enabled) ─────
+# ── Step 4: orchestrator (hosts the full dashboard, holds the cluster open) ─
# stdin is the FIFO; we hold its write end open on fd 3 so hold_open() never
# sees EOF and the cluster stays up until we tear down.
exec 3<>"$FIFO"
echo "demo-fleet: launching orchestrator + ${NUM_STAGES} stage containers (run_id=$RUN_ID)"
# The orchestrator (and, via inheritance, docker-gpu-node.sh) read these from
-# the environment. PP_HOLD keeps the cluster up; the SWACTOR_DIAG_* vars enable
-# each stage's in-VM monitor so real host metrics ship to the collector.
+# the environment. PP_HOLD keeps the cluster up; PP_DASHBOARD makes the
+# orchestrator host the full swactor dashboard locally; the SWACTOR_DIAG_* vars
+# point each stage's in-VM monitor at the collector (the off-box sink) and give
+# the orchestrator the same URL to push its distribution snapshot to and to pull
+# the fleet model from for its Fleet tab.
export PP_HOLD=1
export PP_WORKER_STUB=1
export PP_DEV=CPU
export PP_IMAGE="$IMAGE"
export PP_CONTAINER_PREFIX="$CONTAINER_PREFIX"
-export SWACTOR_DIAG_COLLECTOR_URL="http://${BIND_HOST}:${PORT}"
+export PP_DASHBOARD=1
+export PP_DASHBOARD_PORT="$DASH_PORT"
+export SWACTOR_DIAG_COLLECTOR_URL="$COLLECTOR_URL"
export SWACTOR_DIAG_RUN_ID="$RUN_ID"
export SWACTOR_DIAG_SPOOL_DIR="$SPOOL_DIR"
export SWACTOR_DIAG_UDP_ECHO="${BIND_HOST}:${UDP_PORT}"
[ -n "${PP_GPUS:-}" ] && export PP_GPUS
[ -n "${PP_DIAG_NETWORK:-}" ] && export PP_DIAG_NETWORK
-"$SMOKE_RUN_BIN" \
+"$ORCHESTRATOR_BIN" \
--seed \
--num-stages "$NUM_STAGES" \
--gpu-node "$SCRIPT_DIR/docker-gpu-node.sh" \
@@ -189,6 +203,31 @@ export SWACTOR_DIAG_UDP_ECHO="${BIND_HOST}:${UDP_PORT}"
<"$FIFO" >"$ORCH_LOG" 2>&1 &
ORCH_PID=$!
+# ── Step 5: wait for the orchestrator's dashboard to bind, then announce + open
+WAITED=0
+until (echo > "/dev/tcp/${BIND_HOST}/${DASH_PORT}") >/dev/null 2>&1; do
+ if ! kill -0 "$ORCH_PID" >/dev/null 2>&1; then
+ echo "demo-fleet: orchestrator exited before its dashboard came up." >&2
+ tail -n 40 "$ORCH_LOG" >&2 || true
+ exit 1
+ fi
+ WAITED=$((WAITED + 1))
+ [ "$WAITED" -ge 30 ] && { echo "demo-fleet: orchestrator dashboard did not bind :${DASH_PORT} in 30s" >&2; tail -n 40 "$ORCH_LOG" >&2; exit 1; }
+ sleep 1
+done
+echo
+echo " ┌─────────────────────────────────────────────────────────────┐"
+echo " │ Full swactor dashboard: $DASH_URL"
+echo " │ (overview / actors / topology / distribution / netmap / fleet)"
+echo " │ Collector fleet board: ${COLLECTOR_URL}/dashboard"
+echo " └─────────────────────────────────────────────────────────────┘"
+echo
+if [ -z "${PP_NO_OPEN:-}" ]; then
+ if command -v xdg-open >/dev/null 2>&1; then (xdg-open "$DASH_URL" >/dev/null 2>&1 &) || true
+ elif command -v open >/dev/null 2>&1; then (open "$DASH_URL" >/dev/null 2>&1 &) || true
+ fi
+fi
+
# ── Step 6: wait until the cluster is converged + held open ────────────────
echo "demo-fleet: waiting for the cluster to converge (first inference drive)…"
WAITED=0
diff --git a/examples/pipeline-parallel-inference/scripts/docker-dashboard-e2e.sh b/examples/pipeline-parallel-inference/scripts/docker-dashboard-e2e.sh
index f56fe5c..d49654c 100755
--- a/examples/pipeline-parallel-inference/scripts/docker-dashboard-e2e.sh
+++ b/examples/pipeline-parallel-inference/scripts/docker-dashboard-e2e.sh
@@ -2,7 +2,7 @@
# docker-dashboard-e2e.sh — the docker-e2e run, held open under the live
# swactor dashboard, one dashboard PER STAGE.
#
-# Brings up `N` stub-mode `pp-gpu-node` containers on localhost and drives
+# Brings up `N` stub-mode `pp-worker` containers on localhost and drives
# one InferenceRequest through them, exactly like `docker-e2e.sh` — but each
# stage serves the live swactor dashboard (PP_STAGE_DASHBOARD) and the
# orchestrator HOLDS after the drive (PP_HOLD). The stage containers run on
@@ -78,8 +78,8 @@ done
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CRATE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
WORKSPACE_DIR="$(cd "$CRATE_DIR/../.." && pwd)"
-SMOKE_RUN_BIN="$CRATE_DIR/target/release/pp-smoke-run"
-GPU_NODE_BIN="$CRATE_DIR/target/release/pp-gpu-node"
+ORCHESTRATOR_BIN="$CRATE_DIR/target/release/pp-orchestrator"
+WORKER_BIN="$CRATE_DIR/target/release/pp-worker"
WORKER_PY="$CRATE_DIR/pp_tinygrad_worker.py"
COLLECTOR_BIN="$WORKSPACE_DIR/target/release/swactor-diag-collector"
POSTPROC_BIN="$WORKSPACE_DIR/target/release/swactor-diag-postproc"
@@ -87,20 +87,20 @@ POSTPROC_BIN="$WORKSPACE_DIR/target/release/swactor-diag-postproc"
# Step 1: build the release artifacts the docker image packages (same set
# docker-e2e.sh builds — the image's COPY needs the diag binaries present).
if [ -z "${PP_SKIP_BUILD:-}" ]; then
- echo "docker-dashboard-e2e: building pp-gpu-node + pp-smoke-run (release)"
+ echo "docker-dashboard-e2e: building pp-worker + pp-orchestrator (release)"
cargo build --manifest-path "$CRATE_DIR/Cargo.toml" --release \
- --bin pp-gpu-node --bin pp-smoke-run
+ --bin pp-worker --bin pp-orchestrator
echo "docker-dashboard-e2e: building swactor-diag-{collector,postproc} (release)"
cargo build --manifest-path "$WORKSPACE_DIR/Cargo.toml" --release \
-p distribution --features collector \
--bin swactor-diag-collector --bin swactor-diag-postproc
fi
-for f in "$SMOKE_RUN_BIN" "$GPU_NODE_BIN" "$WORKER_PY" "$COLLECTOR_BIN" "$POSTPROC_BIN"; do
+for f in "$ORCHESTRATOR_BIN" "$WORKER_BIN" "$WORKER_PY" "$COLLECTOR_BIN" "$POSTPROC_BIN"; do
[ -f "$f" ] || { echo "docker-dashboard-e2e: missing $f" >&2; exit 1; }
done
# Step 2: build the layered image (heavy CUDA base, then thin code layer).
-# The stage dashboard lives in the pp-gpu-node binary baked into this image,
+# The stage dashboard lives in the pp-worker binary baked into this image,
# so a stale image without it will show nothing — rebuild unless you know the
# current image already carries the dashboard-enabled binary.
if [ -z "${PP_SKIP_IMAGE_BUILD:-}" ]; then
@@ -131,7 +131,7 @@ for ((k = 0; k < NUM_STAGES; k++)); do
echo " stage ${k}: http://localhost:$((PORT_BASE + k))"
done
-# Step 4: drive pp-smoke-run with the docker shim. The orchestrator serves its
+# Step 4: drive pp-orchestrator with the docker shim. The orchestrator serves its
# own dashboard (PP_DASHBOARD) — including the live SWIM distribution graph and
# message tallies — and each stage serves its own (PP_STAGE_DASHBOARD). PP_HOLD
# makes the orchestrator block at the end, ticking the driver so the
@@ -146,7 +146,7 @@ PP_DASHBOARD=1 \
PP_DASHBOARD_PORT="$ORCH_PORT" \
PP_STAGE_DASHBOARD=1 \
PP_STAGE_DASHBOARD_PORT_BASE="$PORT_BASE" \
-"$SMOKE_RUN_BIN" \
+"$ORCHESTRATOR_BIN" \
--seed \
--num-stages "$NUM_STAGES" \
--gpu-node "$SCRIPT_DIR/docker-gpu-node.sh" \
diff --git a/examples/pipeline-parallel-inference/scripts/docker-diag-e2e.sh b/examples/pipeline-parallel-inference/scripts/docker-diag-e2e.sh
index 97f468d..5a7c588 100755
--- a/examples/pipeline-parallel-inference/scripts/docker-diag-e2e.sh
+++ b/examples/pipeline-parallel-inference/scripts/docker-diag-e2e.sh
@@ -3,7 +3,7 @@
#
# Brings up:
# - `swactor-diag-collector` in a container (HTTP 9080 + UDP 9081)
-# - `pp-smoke-run` on the host, in seed mode with N stub-mode stage
+# - `pp-orchestrator` on the host, in seed mode with N stub-mode stage
# children spawned via `docker-gpu-node.sh` (each its own
# container on `--network host`)
#
@@ -95,8 +95,8 @@ CRATE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
WORKSPACE_DIR="$(cd "$CRATE_DIR/../.." && pwd)"
COMPOSE_FILE="$CRATE_DIR/docker-compose.diag.yml"
-SMOKE_RUN_BIN="$CRATE_DIR/target/release/pp-smoke-run"
-GPU_NODE_BIN="$CRATE_DIR/target/release/pp-gpu-node"
+ORCHESTRATOR_BIN="$CRATE_DIR/target/release/pp-orchestrator"
+WORKER_BIN="$CRATE_DIR/target/release/pp-worker"
WORKER_PY="$CRATE_DIR/pp_tinygrad_worker.py"
COLLECTOR_BIN="$WORKSPACE_DIR/target/release/swactor-diag-collector"
POSTPROC_BIN="$WORKSPACE_DIR/target/release/swactor-diag-postproc"
@@ -104,15 +104,15 @@ POSTPROC_BIN="$WORKSPACE_DIR/target/release/swactor-diag-postproc"
# Step 1: build release artifacts the image will package. The pp binaries
# live in their own workspace; the distribution binaries live at the top.
if [ -z "${PP_SKIP_BUILD:-}" ]; then
- echo "docker-diag-e2e: cargo build pp-smoke-run + pp-gpu-node (release)"
+ echo "docker-diag-e2e: cargo build pp-orchestrator + pp-worker (release)"
cargo build --manifest-path "$CRATE_DIR/Cargo.toml" --release \
- --bin pp-gpu-node --bin pp-smoke-run
+ --bin pp-worker --bin pp-orchestrator
echo "docker-diag-e2e: cargo build swactor-diag-{collector,postproc} (release, --features collector)"
cargo build --manifest-path "$WORKSPACE_DIR/Cargo.toml" --release \
-p distribution --features collector \
--bin swactor-diag-collector --bin swactor-diag-postproc
fi
-for f in "$SMOKE_RUN_BIN" "$GPU_NODE_BIN" "$WORKER_PY" "$COLLECTOR_BIN" "$POSTPROC_BIN"; do
+for f in "$ORCHESTRATOR_BIN" "$WORKER_BIN" "$WORKER_PY" "$COLLECTOR_BIN" "$POSTPROC_BIN"; do
[ -f "$f" ] || { echo "docker-diag-e2e: missing $f" >&2; exit 1; }
done
@@ -190,7 +190,7 @@ if [ "$USE_COMPOSE" = 1 ]; then
"${COMPOSE[@]}" -f "$COMPOSE_FILE" up -d --remove-orphans collector
else
# --entrypoint runs the collector directly; the default image entrypoint
- # (pp_entrypoint.sh) would ignore these args and launch pp-gpu-node.
+ # (pp_entrypoint.sh) would ignore these args and launch pp-worker.
docker run -d --rm \
--name "$COLLECTOR_NAME" \
--network "$DIAG_NETWORK" \
@@ -223,7 +223,7 @@ until (echo > /dev/tcp/127.0.0.1/9080) >/dev/null 2>&1; do
done
echo "docker-diag-e2e: collector ready"
-# Step 5: drive pp-smoke-run with diagnostics env vars set. Stage children
+# Step 5: drive pp-orchestrator with diagnostics env vars set. Stage children
# pick up the same vars via docker-gpu-node.sh's `-e` forwarders.
OUTPUT_DIR="$(mktemp -d)"
STDOUT_LOG="$OUTPUT_DIR/stdout.log"
@@ -244,7 +244,7 @@ SWACTOR_DIAG_COLLECTOR_URL="http://127.0.0.1:9080" \
SWACTOR_DIAG_RUN_ID="$RUN_ID" \
SWACTOR_DIAG_SPOOL_DIR="$OUTPUT_DIR/spool" \
SWACTOR_DIAG_UDP_ECHO="127.0.0.1:9081" \
-"$SMOKE_RUN_BIN" \
+"$ORCHESTRATOR_BIN" \
--seed \
--num-stages "$NUM_STAGES" \
--gpu-node "$SCRIPT_DIR/docker-gpu-node.sh" \
@@ -252,20 +252,20 @@ SWACTOR_DIAG_UDP_ECHO="127.0.0.1:9081" \
--prompt "$PROMPT" \
--max-tokens "$MAX_TOKENS" \
>"$STDOUT_LOG" 2>"$STDERR_LOG"
-SMOKE_STATUS=$?
+ORCH_STATUS=$?
set -e
-if [ "$SMOKE_STATUS" -ne 0 ]; then
- echo "docker-diag-e2e: pp-smoke-run exited $SMOKE_STATUS" >&2
+if [ "$ORCH_STATUS" -ne 0 ]; then
+ echo "docker-diag-e2e: pp-orchestrator exited $ORCH_STATUS" >&2
echo "----- stdout -----" >&2
cat "$STDOUT_LOG" >&2
echo "----- stderr (last 60) -----" >&2
tail -n 60 "$STDERR_LOG" >&2
exit 1
fi
-echo "docker-diag-e2e: pp-smoke-run exited 0"
+echo "docker-diag-e2e: pp-orchestrator exited 0"
if [ -n "${PP_DIAG_VERBOSE:-}" ]; then
- echo "----- pp-smoke-run stderr (last 30) -----"
+ echo "----- pp-orchestrator stderr (last 30) -----"
tail -n 30 "$STDERR_LOG"
fi
@@ -295,7 +295,7 @@ while true; do
else
docker logs "$COLLECTOR_NAME" 2>&1 | tail -n 50 >&2 || true
fi
- echo "----- pp-smoke-run stderr (last 60) -----" >&2
+ echo "----- pp-orchestrator stderr (last 60) -----" >&2
tail -n 60 "$STDERR_LOG" >&2 || true
exit 1
fi
diff --git a/examples/pipeline-parallel-inference/scripts/docker-e2e.sh b/examples/pipeline-parallel-inference/scripts/docker-e2e.sh
index fa5889b..84039a4 100755
--- a/examples/pipeline-parallel-inference/scripts/docker-e2e.sh
+++ b/examples/pipeline-parallel-inference/scripts/docker-e2e.sh
@@ -1,8 +1,8 @@
#!/usr/bin/env bash
# docker-e2e.sh — the Stage 11 pre-deploy gate.
#
-# Brings up `N` stub-mode `pp-gpu-node` containers on localhost, drives
-# one InferenceRequest through them via `pp-smoke-run --seed`, and tears
+# Brings up `N` stub-mode `pp-worker` containers on localhost, drives
+# one InferenceRequest through them via `pp-orchestrator --seed`, and tears
# everything down. The image is built locally from the workspace's
# release artifacts; no GPU, no tinygrad, no GGUF required.
#
@@ -63,8 +63,8 @@ fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CRATE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
WORKSPACE_DIR="$(cd "$CRATE_DIR/../.." && pwd)"
-SMOKE_RUN_BIN="$CRATE_DIR/target/release/pp-smoke-run"
-GPU_NODE_BIN="$CRATE_DIR/target/release/pp-gpu-node"
+ORCHESTRATOR_BIN="$CRATE_DIR/target/release/pp-orchestrator"
+WORKER_BIN="$CRATE_DIR/target/release/pp-worker"
WORKER_PY="$CRATE_DIR/pp_tinygrad_worker.py"
COLLECTOR_BIN="$WORKSPACE_DIR/target/release/swactor-diag-collector"
POSTPROC_BIN="$WORKSPACE_DIR/target/release/swactor-diag-postproc"
@@ -75,15 +75,15 @@ POSTPROC_BIN="$WORKSPACE_DIR/target/release/swactor-diag-postproc"
# present even for this stub run. The pp binaries live in this crate's
# workspace; the diagnostics binaries live at the repo root.
if [ -z "${PP_SKIP_BUILD:-}" ]; then
- echo "docker-e2e: building pp-gpu-node + pp-smoke-run (release)"
+ echo "docker-e2e: building pp-worker + pp-orchestrator (release)"
cargo build --manifest-path "$CRATE_DIR/Cargo.toml" --release \
- --bin pp-gpu-node --bin pp-smoke-run
+ --bin pp-worker --bin pp-orchestrator
echo "docker-e2e: building swactor-diag-{collector,postproc} (release, --features collector)"
cargo build --manifest-path "$WORKSPACE_DIR/Cargo.toml" --release \
-p distribution --features collector \
--bin swactor-diag-collector --bin swactor-diag-postproc
fi
-for f in "$SMOKE_RUN_BIN" "$GPU_NODE_BIN" "$WORKER_PY" "$COLLECTOR_BIN" "$POSTPROC_BIN"; do
+for f in "$ORCHESTRATOR_BIN" "$WORKER_BIN" "$WORKER_PY" "$COLLECTOR_BIN" "$POSTPROC_BIN"; do
[ -f "$f" ] || { echo "docker-e2e: missing $f" >&2; exit 1; }
done
@@ -117,7 +117,7 @@ cleanup_containers() {
}
cleanup_containers
-# Step 4: drive pp-smoke-run with the docker shim as its --gpu-node.
+# Step 4: drive pp-orchestrator with the docker shim as its --gpu-node.
# The shim consults PP_IMAGE / PP_CONTAINER_PREFIX / PP_DEV from its env.
OUTPUT_DIR="$(mktemp -d)"
STDOUT_LOG="$OUTPUT_DIR/stdout.log"
@@ -135,7 +135,7 @@ if [ -n "${PP_REAL:-}" ]; then
PP_DEV=CUDA \
PP_GPUS="${PP_GPUS:-all}" \
PP_MODEL_CACHE_DIR="$PP_MODEL_CACHE_DIR" \
- "$SMOKE_RUN_BIN" \
+ "$ORCHESTRATOR_BIN" \
--seed \
--num-stages "$NUM_STAGES" \
--gpu-node "$SCRIPT_DIR/docker-gpu-node.sh" \
@@ -148,7 +148,7 @@ else
PP_IMAGE="$IMAGE" \
PP_CONTAINER_PREFIX="$PREFIX" \
PP_DEV=CPU \
- "$SMOKE_RUN_BIN" \
+ "$ORCHESTRATOR_BIN" \
--seed \
--num-stages "$NUM_STAGES" \
--gpu-node "$SCRIPT_DIR/docker-gpu-node.sh" \
@@ -157,11 +157,11 @@ else
--max-tokens "$MAX_TOKENS" \
>"$STDOUT_LOG" 2>"$STDERR_LOG"
fi
-SMOKE_STATUS=$?
+ORCH_STATUS=$?
set -e
-if [ $SMOKE_STATUS -ne 0 ]; then
- echo "docker-e2e: pp-smoke-run exited $SMOKE_STATUS" >&2
+if [ $ORCH_STATUS -ne 0 ]; then
+ echo "docker-e2e: pp-orchestrator exited $ORCH_STATUS" >&2
echo "----- stdout -----" >&2
cat "$STDOUT_LOG" >&2
echo "----- stderr (last 60) -----" >&2
diff --git a/examples/pipeline-parallel-inference/scripts/docker-gpu-node.sh b/examples/pipeline-parallel-inference/scripts/docker-gpu-node.sh
index b59953f..328e6a9 100755
--- a/examples/pipeline-parallel-inference/scripts/docker-gpu-node.sh
+++ b/examples/pipeline-parallel-inference/scripts/docker-gpu-node.sh
@@ -1,9 +1,9 @@
#!/usr/bin/env bash
-# docker-gpu-node.sh — shim that pp-smoke-run can spawn instead of the
-# pp-gpu-node binary directly. Boots one pp-gpu-node container per stage
+# docker-gpu-node.sh — shim that pp-orchestrator can spawn instead of the
+# pp-worker binary directly. Boots one pp-worker container per stage
# on the host network so iroh can dial without NAT.
#
-# Required env (forwarded by pp-smoke-run):
+# Required env (forwarded by pp-orchestrator):
# STAGE, NUM_STAGES, SEED_ADDR, SEED_DIRECT, MAX_TOKENS
# Optional env (forwarded if present):
# MODEL, PP_WORKER_STUB, WORKER_CMD,
@@ -80,10 +80,10 @@ mkdir -p "$CACHE_DIR"
#
# --entrypoint runs the binary directly, bypassing the image's default
# pp_entrypoint.sh (sshd + postmortem hold). That supervisor is for remote
-# vast.ai nodes; a local docker stage should exit cleanly when pp-gpu-node
+# vast.ai nodes; a local docker stage should exit cleanly when pp-worker
# does so --rm reaps it and the E2E's no-leftover-container check holds.
exec docker run --rm --init \
- --entrypoint /usr/local/bin/pp-gpu-node \
+ --entrypoint /usr/local/bin/pp-worker \
--name "$NAME" \
--network "${PP_DIAG_NETWORK:-host}" \
"${GPU_ARGS[@]}" \
diff --git a/examples/pipeline-parallel-inference/scripts/remote-collector-fleet.sh b/examples/pipeline-parallel-inference/scripts/remote-collector-fleet.sh
new file mode 100755
index 0000000..b979571
--- /dev/null
+++ b/examples/pipeline-parallel-inference/scripts/remote-collector-fleet.sh
@@ -0,0 +1,179 @@
+#!/usr/bin/env bash
+# remote-collector-fleet.sh — like demo-fleet.sh, but the diagnostics collector
+# lives OFF-box on the real VPS (prod topology) instead of on localhost.
+#
+# Brings up, on THIS machine, the docker stage fleet you would normally deploy
+# (N stub-mode pp-worker containers on --network host) plus pp-orchestrator
+# hosting the FULL swactor dashboard locally. Every stage's in-VM monitor ships
+# its VastaiSample/VastaiLogs records to the REMOTE collector, and the
+# orchestrator's Fleet tab subscribes to that same remote collector's SSE
+# stream (/diag/stream/) and folds the records — so the Fleet tab is
+# exercised end-to-end against the production collector over the public WAN.
+#
+# The collector is NOT started here; it must already be running on the VPS and
+# its port reachable (ufw). This is the "test the prod collector locally" path.
+#
+# Usage:
+# examples/pipeline-parallel-inference/scripts/remote-collector-fleet.sh [N]
+#
+# Environment overrides:
+# PP_COLLECTOR_HOST VPS host running swactor-diag-collector (default 139.59.195.69)
+# PP_COLLECTOR_PORT collector HTTP port (default 9080)
+# PP_COLLECTOR_UDP collector UDP echo port (default 9081)
+# PP_RUN_ID diagnostics run id (default remote-fleet-)
+# PP_DIAG_IMAGE code image tag (default swactor-pp-gpu:latest)
+# PP_BASE_IMAGE base image tag (default swactor-pp-base:cuda12.6)
+# PP_SKIP_BUILD skip cargo build (reuse target/)
+# PP_SKIP_IMAGE_BUILD skip docker image build (reuse tag)
+# PP_DASHBOARD_PORT orchestrator dashboard port (default 9095)
+# PP_PROMPT inference prompt (default "remote fleet demo")
+# PP_MAX_TOKENS decode token cap (default 4)
+# PP_NO_OPEN if set, don't open a browser
+set -euo pipefail
+
+NUM_STAGES="${1:-3}"
+COLLECTOR_HOST="${PP_COLLECTOR_HOST:-139.59.195.69}"
+COLLECTOR_PORT="${PP_COLLECTOR_PORT:-9080}"
+COLLECTOR_UDP="${PP_COLLECTOR_UDP:-9081}"
+RUN_ID="${PP_RUN_ID:-remote-fleet-$(date +%s)}"
+IMAGE="${PP_DIAG_IMAGE:-swactor-pp-gpu:latest}"
+BASE_IMAGE="${PP_BASE_IMAGE:-swactor-pp-base:cuda12.6}"
+PROMPT="${PP_PROMPT:-remote fleet demo}"
+MAX_TOKENS="${PP_MAX_TOKENS:-4}"
+DASH_PORT="${PP_DASHBOARD_PORT:-9095}"
+CONTAINER_PREFIX="remote-fleet-stage"
+COLLECTOR_URL="http://${COLLECTOR_HOST}:${COLLECTOR_PORT}"
+DASH_URL="http://127.0.0.1:${DASH_PORT}/"
+
+if ! [[ "$NUM_STAGES" =~ ^[0-9]+$ ]] || [ "$NUM_STAGES" -lt 2 ]; then
+ echo "remote-fleet: N must be an integer >= 2 (seed mode needs >=2 stages), got '$NUM_STAGES'" >&2
+ exit 2
+fi
+if ! command -v docker >/dev/null 2>&1; then echo "remote-fleet: docker not on PATH" >&2; exit 2; fi
+if ! docker info >/dev/null 2>&1; then echo "remote-fleet: docker daemon unreachable" >&2; exit 2; fi
+if (exec 3<>"/dev/tcp/127.0.0.1/${DASH_PORT}") 2>/dev/null; then
+ exec 3>&- 3<&-
+ echo "remote-fleet: dashboard port ${DASH_PORT} in use. Pick another: PP_DASHBOARD_PORT=9096 $0 ${NUM_STAGES}" >&2
+ exit 2
+fi
+
+# Preflight: the remote collector must be reachable, else the Fleet tab and the
+# stages' shipping will silently get nothing. Fail loudly here instead.
+echo "remote-fleet: checking remote collector at ${COLLECTOR_URL} …"
+if ! curl -fsS -m 8 -o /dev/null "${COLLECTOR_URL}/diag/runs"; then
+ echo "remote-fleet: cannot reach ${COLLECTOR_URL}/diag/runs — is the collector up and the port open (ufw)?" >&2
+ exit 1
+fi
+echo "remote-fleet: remote collector reachable."
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+CRATE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+WORKSPACE_DIR="$(cd "$CRATE_DIR/../.." && pwd)"
+ORCHESTRATOR_BIN="$CRATE_DIR/target/release/pp-orchestrator"
+WORKER_BIN="$CRATE_DIR/target/release/pp-worker"
+WORKER_PY="$CRATE_DIR/pp_tinygrad_worker.py"
+
+if [ -z "${PP_SKIP_BUILD:-}" ]; then
+ echo "remote-fleet: cargo build pp-orchestrator + pp-worker (release)"
+ cargo build --manifest-path "$CRATE_DIR/Cargo.toml" --release --bin pp-worker --bin pp-orchestrator
+fi
+for f in "$ORCHESTRATOR_BIN" "$WORKER_BIN" "$WORKER_PY"; do
+ [ -f "$f" ] || { echo "remote-fleet: missing $f" >&2; exit 1; }
+done
+if [ -z "${PP_SKIP_IMAGE_BUILD:-}" ]; then
+ echo "remote-fleet: docker build $BASE_IMAGE (base)"
+ docker build -f "$CRATE_DIR/Dockerfile.base" -t "$BASE_IMAGE" "$WORKSPACE_DIR"
+ echo "remote-fleet: docker build $IMAGE (code)"
+ docker build -f "$CRATE_DIR/Dockerfile" --build-arg "BASE_IMAGE=$BASE_IMAGE" -t "$IMAGE" "$WORKSPACE_DIR"
+fi
+
+WORKDIR="$(mktemp -d -t remote-fleet.XXXXXX)"
+SPOOL_DIR="$WORKDIR/spool"
+ORCH_LOG="$WORKDIR/orch.log"
+FIFO="$WORKDIR/orch.stdin"
+mkdir -p "$SPOOL_DIR"
+mkfifo "$FIFO"
+ORCH_PID=""
+CLEANED=""
+cleanup() {
+ [ -n "$CLEANED" ] && return 0
+ CLEANED=1
+ set +e
+ echo; echo "remote-fleet: tearing down…"
+ local ids
+ ids=$(docker ps -aq --filter "name=^${CONTAINER_PREFIX}-[0-9]+$")
+ [ -n "$ids" ] && docker rm -f $ids >/dev/null 2>&1
+ [ -n "$ORCH_PID" ] && kill "$ORCH_PID" >/dev/null 2>&1
+ exec 3>&- 2>/dev/null
+ [ -d "$WORKDIR" ] && rm -rf "$WORKDIR"
+ set -e
+ echo "remote-fleet: done."
+}
+trap cleanup EXIT
+trap 'exit 130' INT TERM
+
+exec 3<>"$FIFO"
+echo "remote-fleet: launching orchestrator + ${NUM_STAGES} stage containers"
+echo "remote-fleet: collector = ${COLLECTOR_URL} run_id = ${RUN_ID}"
+export PP_HOLD=1
+export PP_WORKER_STUB=1
+export PP_DEV=CPU
+export PP_IMAGE="$IMAGE"
+export PP_CONTAINER_PREFIX="$CONTAINER_PREFIX"
+export PP_DASHBOARD=1
+export PP_DASHBOARD_PORT="$DASH_PORT"
+export SWACTOR_DIAG_COLLECTOR_URL="$COLLECTOR_URL"
+export SWACTOR_DIAG_RUN_ID="$RUN_ID"
+export SWACTOR_DIAG_SPOOL_DIR="$SPOOL_DIR"
+export SWACTOR_DIAG_UDP_ECHO="${COLLECTOR_HOST}:${COLLECTOR_UDP}"
+"$ORCHESTRATOR_BIN" \
+ --seed --num-stages "$NUM_STAGES" \
+ --gpu-node "$SCRIPT_DIR/docker-gpu-node.sh" \
+ --worker "$WORKER_PY" \
+ --prompt "$PROMPT" --max-tokens "$MAX_TOKENS" \
+ <"$FIFO" >"$ORCH_LOG" 2>&1 &
+ORCH_PID=$!
+
+WAITED=0
+until (echo > "/dev/tcp/127.0.0.1/${DASH_PORT}") >/dev/null 2>&1; do
+ if ! kill -0 "$ORCH_PID" >/dev/null 2>&1; then
+ echo "remote-fleet: orchestrator exited before its dashboard came up." >&2
+ tail -n 40 "$ORCH_LOG" >&2 || true
+ exit 1
+ fi
+ WAITED=$((WAITED + 1))
+ [ "$WAITED" -ge 30 ] && { echo "remote-fleet: dashboard did not bind :${DASH_PORT} in 30s" >&2; tail -n 40 "$ORCH_LOG" >&2; exit 1; }
+ sleep 1
+done
+echo
+echo " ┌─────────────────────────────────────────────────────────────┐"
+echo " │ Full swactor dashboard: $DASH_URL"
+echo " │ Fleet tab pulls from remote collector: ${COLLECTOR_URL}/diag/stream/${RUN_ID}"
+echo " │ Remote collector board: ${COLLECTOR_URL}/dashboard?run=${RUN_ID}"
+echo " └─────────────────────────────────────────────────────────────┘"
+echo
+if [ -z "${PP_NO_OPEN:-}" ]; then
+ if command -v xdg-open >/dev/null 2>&1; then (xdg-open "$DASH_URL" >/dev/null 2>&1 &) || true
+ elif command -v open >/dev/null 2>&1; then (open "$DASH_URL" >/dev/null 2>&1 &) || true
+ fi
+fi
+
+echo "remote-fleet: waiting for the cluster to converge (first inference drive)…"
+WAITED=0
+until grep -q "holding cluster open" "$ORCH_LOG" 2>/dev/null; do
+ if ! kill -0 "$ORCH_PID" >/dev/null 2>&1; then
+ echo "remote-fleet: orchestrator exited before holding — drive failed." >&2
+ tail -n 40 "$ORCH_LOG" >&2 || true
+ exit 1
+ fi
+ WAITED=$((WAITED + 1))
+ [ "$WAITED" -ge 180 ] && { echo "remote-fleet: cluster did not converge within 180s" >&2; tail -n 40 "$ORCH_LOG" >&2; exit 1; }
+ sleep 1
+done
+RUNNING=$(docker ps -q --filter "name=^${CONTAINER_PREFIX}-[0-9]+$" | wc -l | tr -d ' ')
+echo
+echo "remote-fleet: ✅ fleet up — ${RUNNING}/${NUM_STAGES} stage containers shipping to ${COLLECTOR_URL}."
+echo "remote-fleet: watch the Fleet tab live at $DASH_URL"
+echo "remote-fleet: press Ctrl+C to tear everything down."
+echo
+wait "$ORCH_PID"
diff --git a/examples/pipeline-parallel-inference/src/bin/pp_smoke_run.rs b/examples/pipeline-parallel-inference/src/bin/pp_orchestrator.rs
similarity index 74%
rename from examples/pipeline-parallel-inference/src/bin/pp_smoke_run.rs
rename to examples/pipeline-parallel-inference/src/bin/pp_orchestrator.rs
index e8778e8..c857acb 100644
--- a/examples/pipeline-parallel-inference/src/bin/pp_smoke_run.rs
+++ b/examples/pipeline-parallel-inference/src/bin/pp_orchestrator.rs
@@ -1,12 +1,12 @@
-//! pp-smoke-run — orchestrator for the pipeline-parallel smoke test.
+//! pp-orchestrator — orchestrator for the pipeline-parallel smoke test.
//!
//! Two modes:
//!
-//! * `--seed` — fully local. Spawns `N` `pp-gpu-node` child processes
+//! * `--seed` — fully local. Spawns `N` `pp-worker` child processes
//! (`STAGE=0..N-1`) talking to a local iroh seed. Uses
//! `RelayMode::Disabled` since direct addresses suffice on localhost.
//! * `--vastai` — rents `N` GPU instances on vast.ai, deploys the
-//! `pp-gpu-node` image to each, and drives the same orchestrator path
+//! `pp-worker` image to each, and drives the same orchestrator path
//! over WAN. The default one-shot destroys all rented instances before
//! exit; `--hold` leaves the cluster running (tracked by a local handle
//! file) so it can be iterated on, and `--teardown` destroys it. See the
@@ -55,6 +55,7 @@ use distribution::diagnostics::Role as DiagRole;
use pipeline_parallel_inference::diag;
use pipeline_parallel_inference::dist_broadcast;
+use pipeline_parallel_inference::fleet_plugin::RemoteVastaiPlugin;
use pipeline_parallel_inference::dist_plugin::{
CountingEmitter, DistDashPlugin, MsgCounts, SharedSnapshot,
};
@@ -94,8 +95,8 @@ fn node_config() -> DistributedNodeConfig {
fn print_usage() {
eprintln!("Usage:");
- eprintln!(" pp-smoke-run --seed [--num-stages N] [--prompt ] [--max-tokens ] [--gpu-node ] [--worker ]");
- eprintln!(" pp-smoke-run --vastai --api-key [--num-stages N] [--gpu \"RTX 3060\"] [--image ] [--prompt ] [--max-tokens ]");
+ eprintln!(" pp-orchestrator --seed [--num-stages N] [--prompt ] [--max-tokens ] [--gpu-node ] [--worker ]");
+ eprintln!(" pp-orchestrator --vastai --api-key [--num-stages N] [--gpu \"RTX 3060\"] [--image ] [--prompt ] [--max-tokens ]");
eprintln!("Cluster lifecycle (--vastai):");
eprintln!(" (default) lease N, drive one run, destroy.");
eprintln!(" --hold lease N, drive, leave running; writes a cluster-handle file.");
@@ -141,14 +142,14 @@ fn parse_args() -> Args {
vastai: false,
num_stages: 2,
api_key: None,
- // RTX 3060 (12GB) is our default deploy-test class: cheapest GPU class
- // with deep, reliable supply on vast.ai (see fleet notes). Settable via
- // PP_GPU in a profile, or --gpu for capacity tests. NOT sized for real
- // model weights.
+ // GPU model pin: off by default (empty = no model filter). Set PP_GPU in
+ // a profile, or --gpu for capacity tests, to restrict to one model. Pair
+ // or replace with PP_GPU_MIN_RAM_MB to select by VRAM instead.
gpu_name: std::env::var("PP_GPU")
.ok()
- .filter(|s| !s.trim().is_empty())
- .unwrap_or_else(|| "RTX 3060".into()),
+ .map(|s| s.trim().to_string())
+ .filter(|s| !s.is_empty())
+ .unwrap_or_default(),
// Image is not baked to a personal registry: it defaults from the
// PP_IMAGE env (the convention the run scripts already use, e.g.
// scripts/docker-e2e.sh), falling back to a registry-less tag. Set it
@@ -272,7 +273,7 @@ fn main() {
// ─── Seed (localhost) mode ────────────────────────────────────────────
-/// Resolve the `pp-gpu-node` binary path. Defaults to a sibling of the
+/// Resolve the `pp-worker` binary path. Defaults to a sibling of the
/// current executable.
fn resolve_gpu_node_path(args: &Args) -> PathBuf {
if let Some(p) = &args.gpu_node_path {
@@ -280,7 +281,7 @@ fn resolve_gpu_node_path(args: &Args) -> PathBuf {
}
let exe = std::env::current_exe().expect("current_exe failed");
let parent = exe.parent().expect("current exe has no parent");
- parent.join("pp-gpu-node")
+ parent.join("pp-worker")
}
/// Resolve the worker script path. Defaults to `pp_tinygrad_worker.py`
@@ -292,7 +293,7 @@ fn resolve_worker_path(args: &Args) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("pp_tinygrad_worker.py")
}
-/// Build a `Command` for a single `pp-gpu-node` child. Captures all the
+/// Build a `Command` for a single `pp-worker` child. Captures all the
/// env-var bookkeeping in one place so the spawn-chain closure is short.
fn build_gpu_node_command(
gpu_node_bin: &PathBuf,
@@ -303,7 +304,7 @@ fn build_gpu_node_command(
ctx: &StageSpawnCtx,
) -> Command {
eprintln!(
- "pp-smoke-run: spawning {} STAGE={} NUM_STAGES={}",
+ "pp-orchestrator: spawning {} STAGE={} NUM_STAGES={}",
gpu_node_bin.display(),
ctx.stage,
ctx.num_stages,
@@ -421,7 +422,7 @@ fn run_seed(args: &Args) -> i32 {
let gpu_node_bin = resolve_gpu_node_path(args);
if !gpu_node_bin.exists() {
eprintln!(
- "pp-smoke-run: pp-gpu-node binary not found at {} (use --gpu-node to override)",
+ "pp-orchestrator: pp-worker binary not found at {} (use --gpu-node to override)",
gpu_node_bin.display()
);
return 1;
@@ -429,7 +430,7 @@ fn run_seed(args: &Args) -> i32 {
let worker_script = resolve_worker_path(args);
if !worker_script.exists() {
eprintln!(
- "pp-smoke-run: worker script not found at {} (use --worker to override)",
+ "pp-orchestrator: worker script not found at {} (use --worker to override)",
worker_script.display()
);
return 1;
@@ -444,7 +445,7 @@ fn run_seed(args: &Args) -> i32 {
}) {
Ok(d) => d,
Err(e) => {
- eprintln!("pp-smoke-run: failed to create iroh driver: {e}");
+ eprintln!("pp-orchestrator: failed to create iroh driver: {e}");
return 1;
}
};
@@ -476,7 +477,7 @@ fn run_seed(args: &Args) -> i32 {
.collect::>()
.join(",");
eprintln!(
- "pp-smoke-run (--seed --num-stages {n}): orchestrator node {my_hex}, direct={direct:?}",
+ "pp-orchestrator (--seed --num-stages {n}): orchestrator node {my_hex}, direct={direct:?}",
n = args.num_stages,
);
@@ -543,10 +544,22 @@ fn run_seed(args: &Args) -> i32 {
)));
let poll_stop = Arc::new(AtomicBool::new(false));
spawn_conn_poller(&driver, Arc::clone(cached), conn_tracker, poll_stop);
+ // Fleet plugin ("Fleet" nav tab): the vast.ai/host-metric fleet view.
+ // In production the collector lives on a VPS; this plugin *pulls* the
+ // collector's already-folded fleet model ~1/s and re-serves it locally,
+ // so the full dashboard shows the fleet beside the live actor views.
+ // Registered unconditionally so the tab exists; the poller only runs
+ // when a collector URL is known (the same one the dist snapshot is
+ // pushed to). Idle URL => the tab just waits for records.
+ let fleet = Arc::new(RemoteVastaiPlugin::new());
+ if let Some(url) = broadcast_url.as_ref() {
+ fleet.spawn_stream(&driver.tokio_handle(), url.clone(), broadcast_run_id());
+ }
+ handle.register_plugin(fleet);
handle.start_http(driver.tokio_handle());
eprintln!(
- "pp-smoke-run: live dashboard on http://localhost:{port} \
- (overview / actors / topology / distribution / netmap)"
+ "pp-orchestrator: live dashboard on http://localhost:{port} \
+ (overview / actors / topology / distribution / netmap / fleet)"
);
}
@@ -560,13 +573,13 @@ fn run_seed(args: &Args) -> i32 {
url.clone(),
broadcast_run_id(),
);
- eprintln!("pp-smoke-run: broadcasting distribution snapshot to {url}");
+ eprintln!("pp-orchestrator: broadcasting distribution snapshot to {url}");
}
let response_inbox = match rt.new_inbox::() {
Ok(i) => i,
Err(e) => {
- eprintln!("pp-smoke-run: new_inbox failed: {e}");
+ eprintln!("pp-orchestrator: new_inbox failed: {e}");
break 'run (1, "new_inbox_error");
}
};
@@ -589,18 +602,18 @@ fn run_seed(args: &Args) -> i32 {
}) {
Ok(g) => g,
Err(e) => {
- eprintln!("pp-smoke-run: {e}");
+ eprintln!("pp-orchestrator: {e}");
break 'run (1, "spawn_chain_error");
}
};
- eprintln!("pp-smoke-run: spawned {} stage children", guard.len());
+ eprintln!("pp-orchestrator: spawned {} stage children", guard.len());
// The chain spawner read the announcement line from each child's
// stdout and kept the pipe draining in a background thread. No
// further stdout pumping needed here.
// Wait for the cluster (orchestrator + N stages) to converge.
eprintln!(
- "pp-smoke-run: waiting for cluster convergence ({} alive peers)...",
+ "pp-orchestrator: waiting for cluster convergence ({} alive peers)...",
args.num_stages,
);
let conv_res = await_convergence_or_child_death(
@@ -620,17 +633,17 @@ fn run_seed(args: &Args) -> i32 {
.node_mut()
.register_name(ORCHESTRATOR_NAME.into(), inbox_addr);
diag::emit_register_name(&driver, ORCHESTRATOR_NAME, inbox_addr, None);
- eprintln!("pp-smoke-run: registered {ORCHESTRATOR_NAME} -> {inbox_addr:?}");
+ eprintln!("pp-orchestrator: registered {ORCHESTRATOR_NAME} -> {inbox_addr:?}");
if let Err(e) = conv_res {
- eprintln!("pp-smoke-run: {e}");
+ eprintln!("pp-orchestrator: {e}");
break 'run (1, "convergence_error");
}
- eprintln!("pp-smoke-run: cluster converged");
+ eprintln!("pp-orchestrator: cluster converged");
// Spec §4.6 + §4.5: gate the request injection on (a) every
// pp-stage-K resolvable and (b) pp-entry resolvable. The
// per-index name is published by each stage only after its
- // worker is ready (pp-gpu-node.rs), so resolution of every
+ // worker is ready (pp-worker.rs), so resolution of every
// pp-stage-K is a faithful "all workers ready" signal. The
// resolve loop polls children too so a stage that dies during
// wiring fails fast instead of waiting out the timeout.
@@ -638,7 +651,7 @@ fn run_seed(args: &Args) -> i32 {
.ok()
.and_then(|s| s.trim().parse().ok())
.unwrap_or(1800);
- eprintln!("pp-smoke-run: waiting for pipeline-wired (all pp-stage-K + {ENTRY_NAME})...");
+ eprintln!("pp-orchestrator: waiting for pipeline-wired (all pp-stage-K + {ENTRY_NAME})...");
let wire_deadline = Instant::now() + Duration::from_secs(roster_deadline_secs);
let mut roster_hex: Vec