Send a Run to a GPU Box

Route a fine-tune to a remote GPU with –remote-host, a remote: YAML block, or the desktop 'Run on' dropdown

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.

Diagram

Same instruction, three ways

💻 –remote-host gpubox

🚚 dispatch

📄 remote: in config.yaml

🖱️ Desktop ‘A remote server’

🖥️ GPU box

✅ lora_model

Quick Start

  1. Set up key auth once

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

    bash
    ssh-copy-id gpubox
    ssh gpubox echo ok   # must print `ok` without a prompt
  2. Send it with a flag
    bash
    praisonai-train llm dataset.json \
        --model unsloth/gemma-2-2b-it-bnb-4bit \
        --remote-host gpubox

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

  3. Or put it in the config file
    yaml
    # config.yaml
    model_name: "unsloth/gemma-2-2b-it-bnb-4bit"
    dataset:
      - name: "yahma/alpaca-cleaned"
    remote:
      host: gpubox
    bash
    praisonai-train llm --config config.yaml
  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.

Diagram

yes

no

🚩 –remote-host / –remote-python / …

📄 remote: block in config.yaml

⚙️ default: python3 / ~/.praisonai-train / gpus=1

🧩 merged block

host settled?

🖥️ dispatch remotely

💻 train here as before

  • 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.

KeyFlagYAMLTypeDefaultDescription
host--remote-hostremote.hoststr(none)SSH alias from ~/.ssh/config. Omit to train locally.
python--remote-pythonremote.pythonstrpython3Interpreter on the remote host.
workdir--remote-workdirremote.workdirstr~/.praisonai-trainDirectory on the remote host to work in.
gpus--remote-gpusremote.gpusint1How many GPUs the run expects to find.

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.

    You wroteShips?
    dataset: "d.jsonl" (local file)Yes — copied to the run dir.
    dataset: [{name: "d.jsonl"}] (local file)Yes — name is the path, exactly as the trainer treats it.
    dataset: [{name: "my-set", data_files: "d.jsonl"}]Yes — data_files is what ships; name stays the label.
    dataset: [{name: "org/alpaca-cleaned"}] (Hub id)No — the far side loads it from the Hub.
    dataset: "/remote/only/path.jsonl" (only on the host)No — nothing to copy.

    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.


Preview Before You Spend the GPU-hours

--dry-run prints the resolved config, including the settled remote: block (credentials redacted), and sends nothing.

bash
praisonai-train llm --config config.yaml --remote-host gpubox --dry-run

The remote: block appears in the preview with the host, interpreter, workdir, and GPU count the run would actually use:

yaml
dataset:
- name: yahma/alpaca-cleaned
model_name: unsloth/gemma-2-2b-it-bnb-4bit
remote:
  gpus: 1
  host: gpubox
  python: python3
  workdir: ~/.praisonai-train

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.


Common Refusals (all before any SSH)

Each of these exits 1 before a connection is opened. The forbidden value is never echoed back.

You wroteWhat happens
remote: {host: h, password: hunter2}Refused — must not carry a credential. This file is copied to the remote host and printed by --dry-run. Use an ssh agent instead. Value never echoed.
remote: {host: "gpu; rm -rf /"}Refused — not a plain SSH alias. Put connection details in ~/.ssh/config and name the alias here.
remote: {host: h, workdir: "$(pwd)"}Refused — the workdir is expanded by the remote shell; only letters, digits, ._-/~ allowed.
remote: {host: h, python: "python; whoami"}Refused — same character set as workdir.
remote: gpubox (a string, not a mapping)Refused — must be remote: {host: gpubox, ...}.
remote: [] (an empty list)Refused — remote: must be a mapping of key: value. An omitted key (or remote: null) means “train here”.
remote: "" (an empty string)Same refusal — a present-but-falsey value is a malformed block, not an absence.
remote: falseSame refusal.
remote: {host: h, gpus: 0}Refused — gpus must be a positive integer.
remote: {host: h, hostname: typo}Refused — unknown key hostname. Known keys: host, python, workdir, gpus.

Best Practices

Put connection details in ~/.ssh/config

Name only the alias in PraisonAI; keep host, user, and key in your ssh config:

Host gpubox
    HostName gpu-01.example.com
    User me
    IdentityFile ~/.ssh/id_ed25519

Then --remote-host gpubox, remote: {host: gpubox}, and the desktop’s alias field all mean the same thing.

Dry-run before you rent the GPU

--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.

Use this for 'kick it off and watch it'

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.

Combine with multi-GPU on the remote box

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.


Related