drive
One master plus Distributed.jl workers. The script is a driver that farms work (e.g. pmap).
julia --project=. -m DistSSHKit drive [options] \
[parent:N] [child:NAME:N...] SCRIPT.jl [script_args...]Also: First Steps · Demo, go, API (drive!, pipeline!), drive --help. Flag vocabulary and a short go vs drive table: User Guide.
vs go / ride: one master plus Distributed workers; child:NAME:N is worker count. Three phases: Load (master include), Publish (defs / using / import / include on workers), Run (main() if defined). No Distributed vocabulary in this file is a warning, not a refusal. plan may still suggest go or ride. Full-file worker include: --sync-script. Opt-in git parity is here only (--require-git). Prepare remotes with setup --rsync or --clone, then --instantiate. One-shot onto an empty/missing path: drive --rsync (instantiates if needed).
Publishing to workers
Default publish (not --sync-script) re-evaluates bare top-level function / macro / struct / using / import / const / include(...) (and short-form methods) on each worker. Top-level work (println, loops, pmap at file scope) stays on the master Load.
Wrapping include(...) in if / || / && hides it from publish:
isdefined(Main, :load_full_config) || include(DEMO_JL)That node is Expr(:||, …), not a bare include call, so workers never see it. drive warns and continues. Use a bare include(...), or --sync-script for a full-file worker include.
@__DIR__ is re-evaluated on each worker: _drive_worker_publish! sets task_local_storage()[:SOURCE_PATH] to that worker’s remapped script path before re-running published source. Prefer @__DIR__-derived consts for sibling files (parent, child:NAME, and DistSSHQueue qhost: trees have different absolute paths). Do not bake a master abspath / @__FILE__ / Base.active_project() into the published source:
const REPO_ROOT = dirname(@__DIR__)
const DEMO_JL = joinpath(REPO_ROOT, "demo.jl")
include(DEMO_JL)Flags
--sync/--rsync: optional pre-run copy;--rsyncinstantiates if needed--sync-script: re-includethe full driver on workers (default publishes definitions only)--require-git: opt-in git parity: dirty-tree warn + remote commit must match local--require-all-hosts: fail unless every explicitparent:N/child:NAME:Njoined, stayed, and collect succeeded (default)--best-effort: allow a partial run (missing join or collect error does not fail)--skip-git-guard: compat no-op (parity already off)-w/--workers N:parent:Nwhen noparenttoken is listed (also-w:N). Listed hosts still need:N.--julia PATH: Julia on SSH workers--mem-headroom N: RAM fraction for memory preflight (default0.75; same assize)--parent-gb N: GB reserved for the parent process (default0.4; same as size)--output-dir PATH: result root →DISTRIBUTED_OUTPUT_DIR(not go batch root). Omitted: the driver may set it ininit_output_dir!; otherwise the run allocates{script}/.distsshkit/drive/<stem>_<UTC>/. Detachedexecute!(:drive)does not pin--output-dirunless you passoutput_dir=(or inheritDISTRIBUTED_OUTPUT_DIR).Run sidecars (
kit.pid,kit.out/kit.err,run.toml) live under{script}/.distsshkit/runs/drive/<stem>_<UTC>/, not in the artifact dir.--log-dir PATH: log directory override--no-log: do not writedrive_<timestamp>.log--package NAME:using NAMEon workers (override Project.toml name)--collect-missing ROOT HOST...: collect-only: remote files absent locally--collect-overwrite ROOT HOST...: collect-only: merge remote tree (overwrite same names)-q/--quiet: hide terminal detail; kit log still written when logging is on--progress: live status (TTY default)--verbose: full detail (non-TTY default)-y/--yes: auto-accept memory-pressure and other prompts--hosts CSV: comma-separated worker specs (same form as CLI tokens /DISTSSHKIT_HOSTS)--hosts-file PATH: append worker specs (child:NAME:Npreserved)-v/--version: print DistSSHKit version and exit-h/--help: full help--sync/--rsyncare mutually exclusive (pre-run sync)--require-gitcannot combine with--rsyncor--skip-git-guard--skip-git-guardis a compat no-op (parity already off) and may combine with--sync/--rsyncDefault pre-run sync and git parity are both none (same idea as
go). Prepare remotes withsetup, or pass--sync/--rsync.--rsyncinstantiates when the copied tree still lacks deps. Use--require-gitonly on git-managed remotes.
DISTSSHKIT_REQUIRE_ALL_HOSTS=1 is the same as --require-all-hosts (already the default when neither flag/env is set). DISTSSHKIT_BEST_EFFORT=1 is the same as --best-effort (cannot combine with the require-all env). DISTSSHKIT_JOBS (default 1) caps concurrent SSH host work for rsync, post-run collect, and size Julia-path detection.
Prerequisites
Run setup --check on new clusters. Remotes need the project tree and an instantiate (setup --instantiate, or drive --rsync onto an empty path). Prefer matching Julia major.minor; align with up when juliaup is already on the host (Requirements).
Workers
Listed tokens need :N. -w N is parent:N when no parent token is listed. Estimate with size, then paste the printed tokens.
- Ctrl-C during run stops the driver (exit 130): interrupt in-flight worker
pmap, wait a few seconds, thenrmprocs. One SIGINT is enough; the live bar does not swallow it. SSH workers on one host share a singlekill -INT. A worker function maycatchInterruptExceptionand save partial state. A loop that does not yield does not see the interrupt until it yields;rmprocsstill runs after the wait. On Julia 1.13, Ctrl-C while the driver waits inpmapdoes not reach this path. The driver exits 1 and does not sendkill -INT. Measured on macOS with a process-group SIGINT. On Julia 1.14 the driver exits 130, but a worker can be removed before it sees the interrupt - Local workers are torn down with
rmprocsat the end of everydriverun - SSH leftover
pkillmatches argvdistsshkit-job:<id>whenjob_id/DISTSSHKIT_JOB_IDis set (drive workers:--eval=#distsshkit-job:<id>). Untaggedjulia --workersweep issetup --cleanup. Skip leftover taggedpkillwithDISTSSHKIT_SKIP_GLOBAL_WORKER_PKILL=1(User Guide) - Machine-wide local kill is
setup --cleanup
Results / collect
After main(): post-run-new collect. Standalone pull via the collect flags above. See User Guide for mode names. SSH / find errors on a host are a collect failure (HostRunResult.ok is false), not an empty success. Default CLI exit is non-zero unless --best-effort.
External watchers: progress: begin / step / item always go to kit.progress (even -q / --no-log). The log still gets those lines only with --progress (TTY default) or DISTSSHKIT_PROGRESS=1. progress: done is always in both. Line format: API (Progress lines).
Collect expands remote ~/… roots on each host before find / rsync so the machine the run starts from never relpaths against a tilde base (same ENV as setup remote path).
Driver script
Expects init_output_dir! / main (and optional hooks). Top-level work on Load is not repeated on workers unless --sync-script. Publish rules and @__DIR__ on workers: Publishing to workers. Details and ENV: drive --help. Embed with drive! / pipeline!.
Wall time
Run drive as usual (-q hides the table). The Time table prints at the end, after job stdout, with a progress DIR line to replay:
julia --project=. -m DistSSHKit drive -y parent:4 \
distsshkit_demos/with_kit/square_echo.jl --n 4
julia --project=. -m DistSSHKit progress DIRDIR is the result root (--output-dir, a driver's init_output_dir!, or {script}/.distsshkit/drive/<stem>_<UTC>/). --progress is the TTY default.
progress: lines end with t=<unix> (API). Drive labels: sync (optional), git, cleanup, workers (addprocs + Julia detect), wait (SSH connection grace; DISTRIBUTED_INIT_DELAY_SEC, default 5; skipped when there are no SSH hosts), init, run, collect. Nested rows under each host: workers / init / collect (and cleanup on remotes). wait is a fixed sleep. Leave DISTRIBUTED_INIT_DELAY_SEC and DISTSSHKIT_JOBS (default 1) unless a remote run gives a reason to change them.
Concurrent runs
Two drive runs (or drive! calls) against the same result root fail fast: a .kit.lock file (this run's pid) is written under the result root, and a second run against the same directory raises immediately instead of interleaving collect output. A lock left behind by a crashed/killed run is detected as stale (dead pid) and reclaimed automatically — no manual cleanup needed. Concurrent runs of the same script get distinct default dirs. Use a shared --output-dir (or the same init_output_dir! path) only when you intend one result root — a second run against that directory raises until the first releases .kit.lock.