Skip to main content
Add --remote-host gpubox to praisonai-train llm, put a remote: block in your config file, or pick “A remote server” in the desktop’s fine-tune form — the same instruction, three ways. The run happens on the other box; your laptop can close.

Quick Start

1

Set up key auth once

The runner never accepts a password prompt — it uses your ssh agent and ~/.ssh/config.
2

Send it with a flag

Streams the log while training. Ctrl-C stops the remote run — not just the tail.
3

Or put it in the config file

4

Or pick 'A remote server' in the desktop

Open Fine-tune a model, set Run on → A remote server, and type the SSH alias. The remote python and remote directory fields reveal themselves with sensible defaults.

What Settles What — flag > YAML > default

The presence of a settled host is what routes the run remotely; there is no separate mode flag.
  • No host → train here, exactly as before.
  • A flag overrides the file. An absent flag does not erase what the YAML says — only non-empty overrides win.
  • A local run in the desktop posts no remote: block, even if you typed a host and then switched back to “This computer”.

Options

Every key is expressible as a flag, in YAML, and in the desktop. All three drive the same code.
workdir is expanded by the remote shell — ~/.praisonai-train lands in the remote user’s home, not yours. It is created with mkdir -p -m 700, so on a shared GPU box the umask does not decide who can read the shipped config and dataset. Only letters, digits, ., _, -, /, and an optional leading ~ are allowed; the runner refuses anything else before any SSH. See PraisonAI PR #4550 for the tilde-expansion fix and the mode-700 hardening.

What Is Shipped, and What Is Not

The dispatcher rewrites the config before sending it so the far side does the right thing.
1

The remote: block is stripped

The remote host reads a config file too. Leaving remote: in would make it find a host and dispatch again. The dispatcher removes it before shipping.
2

A local dataset is copied; a Hub dataset is not

The dispatcher walks the resolved dataset — a string or a list-of-mappings — and copies the first local file it finds, whether it was named on the CLI or only inside config.yaml. A HuggingFace id or a path that lives only on the remote host is not a local file, so it is left alone. Name a local file explicitly with data_files when you want name to stay a label.A dataset: "path.jsonl" string is normalised to [{name: "path.jsonl"}] in the shipped config for the remote trainer.
3

Credentials are refused up front

password, passphrase, token, key, secret, identity_file, private_key in the remote: block are refused before any SSH — the file is shipped to the remote host and printed by --dry-run, so a credential in it would leak. Use your ssh agent and ~/.ssh/config.
The dispatcher writes to a NamedTemporaryFile, never to a config.yaml in the directory you launched from — a remote run never rewrites a file where you happen to be standing.

Preview Before You Spend the GPU-hours

--dry-run prints the resolved config, including the settled remote: block (credentials redacted), and sends nothing.
The remote: block appears in the preview with the host, interpreter, workdir, and GPU count the run would actually use:
A bad remote: block fails here too — --dry-run resolves and validates it, so a malformed block exits 1 with Bad remote settings before dispatch rather than after an hour of rented GPU. Answers “what am I about to do, and where?” before an hour of rented GPU.

Ctrl-C Stops the Remote Run

Ctrl-C during the log tail is not “just close the local tab”. The dispatcher forwards SIGINT / SIGTERM to the remote runner, which stops the training process — the same as the desktop Stop button.
Without this, the signal would end the tail and leave the run holding a rented GPU while reporting “cancelled” for a job that is still training. Signal forwarding falls back cleanly on non-main threads and platforms that refuse signal.signal.
A remote run that ends failed (exit N) now exits 1 locally too, so $? and CI gating see the failure. Earlier the CLI printed the status and then exited 0, reporting success for a run that had died.

Common Refusals (all before any SSH)

Each of these exits 1 before a connection is opened. The forbidden value is never echoed back.
The credential check runs before the unknown-key check on purpose. A forbidden key like password is also technically “unknown”, but telling the user “you misspelled it” is the wrong advice — the fix for a typo is to spell it right, which here would mean trying harder to put a password in a file that gets shipped to another machine.

Best Practices

Name only the alias in PraisonAI; keep host, user, and key in your ssh config:
Then --remote-host gpubox, remote: {host: gpubox}, and the desktop’s alias field all mean the same thing.
--dry-run prints the resolved config with the remote: block and sends nothing. It catches a wrong host or wrong workdir before an hour of rented GPU.
This shortcut is one command — good for a single job you want to launch and tail. For manual preflight, detached start, reattach later, fetch the adapter, and stop, use the standalone praisonai-train remote sub-app.
praisonai-train llm --config config.yaml --remote-host gpubox --remote-gpus 4 sets the GPU expectation for the remote run. See Multi-GPU for the launcher side.

praisonai-train Package

The full praisonai-train CLI including llm, serve, export, and remote.

Remote Sub-app

Manual preflight, detached start, reattach, fetch, stop.

Multi-GPU Training

torchrun across every GPU on the host.

Train

Local training flow and full config.yaml reference.