You have a machine with a good GPU. It sits under a desk, or in a rack, or in a cloud account. Your laptop has the code. All you want is to send the work there, let it run, and get the results back.
So you do what everyone does.
scp -r ./src gpu-box:~/work ssh gpu-box "cd ~/work && make -j8 linux" scp -r gpu-box:~/work/out ./result It works. But look at what you just did.
You moved the files in by hand, and you moved the results out by hand. You had to remember which folder to bring back, and ~/work is still sitting on the box. Every job, every time.
And to run one make, you needed a full shell. When a teammate needs the same build, you add their public key to authorized_keys. They only wanted to run make, but now they have a shell on your GPU box, and they can run anything there.
That was the easy version. 😅
Two ends, and nothing in the middle
When you look for a better way, you find two kinds of tools.
🔑 SSH and Ansible are too open. Whoever can send work can run anything. And the files are your problem: you copy them in, you copy them out, you clean up.
🏗️ Slurm is too heavy. It is powerful, but it needs a cluster, shared storage, and someone to keep it all alive. For "run this build on that machine", it is a lot.
Much of the real work sits between them: builds, tests, renders, conversions, model runs. Work that starts, runs, and ends.
| SSH, Ansible | GRID | Slurm | |
|---|---|---|---|
| What runs | any command | only the node's fixed cmds | any batch script |
| Files | you copy them | in and out with the job | a shared filesystem |
| To set up | SSH access | one binary per side | a cluster |
GRID (Generic Runtime Intent-Driven) is that middle column. It has two sides:
- a hub takes your jobs, picks where each one runs, and keeps the results. It can be your laptop or a small server;
- a node is a machine, container, or VM that offers some commands and runs jobs with them.
Narrower than a shell. Lighter than a cluster. And the files are part of the job.
Narrower than a shell
Remember the teammate in authorized_keys. With SSH, the person who sends the work decides what runs. With GRID, the machine decides.
A node names its commands, its cmds, in its own config:
# /etc/grid/node.toml [cmds.build] exec = ["make", "-j8"] A job can only ask for a cmd by name. It can add args and env, nothing more:
[[steps]] cmd = "build" args = ["linux"] # the node runs: make -j8 linux A step that names a cmd the node does not offer is refused. There is no other way in.
| SSH | GRID | |
|---|---|---|
| Who decides what runs | whoever logs in | whoever runs the node |
| What a job can send | any command | args and env for a known cmd |
| What a teammate gets | a shell | a way to ask |
🛡️ So a teammate, a script, or another service can send work to your machines without getting the machines.
One honest note: a node is only as narrow as its cmds. If you offer exec = ["sh", "-c"], the args become a script, and you have given the shell back. Keep cmds specific. If you don't trust the work, run the node in a container or a VM.
A job is an intent, not a script. That is where the name comes from.
Lighter than a cluster
One binary, grid, runs either side, on macOS, Linux, and Windows. You don't need a scheduler, a shared filesystem, or certificates. Hub and node talk over one TCP port, 9420, encrypted end to end. Each side trusts only the other's public key, from a known IP.
Adding machines needs no script. Give each node tags, the kinds of work it does, and a job names the tag it needs. The hub sends it to a node with that tag and a free slot. If none is free, the job waits in the queue.
The cmd runs on the node, not inside your connection. Nodes report, and the hub decides:
| When | Then |
|---|---|
| a connection drops | only that call is sent again; the cmd keeps running on the node |
| the node goes away | the job starts over on another node, up to retries more times |
| the hub restarts | its jobs are picked up again, from where they were |
| a step fails | the rest are skipped and the job is failed; it is not retried |
Light does not mean fragile.
Files in, files out — fully covered
This is the part SSH leaves to you, and a cluster hides behind shared storage. In GRID, the files are part of the job.
Every job runs in its own clean folder on the node, its space. The job file reads in the order it runs:
# build.toml tag = "build" # any node with the "build" tag retries = 2 [[push]] # files in, before the steps from = "./src" # next to this file, on the hub's side to = "src" # in the space [[steps]] # the node's cmds, in order cmd = "build" args = ["linux"] env = { CC = "clang" } [[pull]] # files out, after the steps path = "out" | Part | Direction | What happens |
|---|---|---|
| push | hub → node | your files are copied into the space |
| steps | on node | the node runs its cmds there, on those files |
| pull | node → hub | the files you named come back to the hub |
Each part can repeat: several pushes, several steps, several pulls.
Now back to the first story:
| By hand | With GRID |
|---|---|
| you copy the files in | push copies them in, before the steps |
| you copy the results out | pull copies them out, after the steps |
| you remember which folder to bring back | the job file names it, once |
you clean up ~/work later | when the job ends, its space is deleted |
And a few things scp never did:
- A broken push or pull resumes where it stopped, and a sha256 checks the whole.
- The hub packs an input once, and knows it again by its files' names, sizes, and times. An unchanged input is not packed again.
- The input is packed when you submit, so you can edit
./srcright away. The job has its own copy. - When a step fails, the files are still pulled, so you can see what went wrong.
- A path that would leave the space (
../, absolute, a symlink out) is rejected. - Pulled files stay on the hub until
grid hub drop, or for 30 days by default.
📦 Big inputs travel once. A model or a dataset that many jobs use can be a share:
[[push]] from = "./model" to = "model" share = true The node keeps it, read only, and links it into every job that pushes the same content. The first job sends it. The next jobs on that node send nothing for it. grid hub tail shows how each push went: hit (the node had it), installed (sent, and kept), or normal.
The share is off by default. A node turns it on with a size, like [share] limit = "10GB". A node without it takes the push as a normal one, and the job runs either way.
Push brings the inputs, the cmds work on them in place, and pull takes the outputs.
What GRID is not
To be honest about it: GRID does not try to be Slurm. Slurm shares a whole cluster among many users, with partitions, accounts, and fair-share scheduling. If you need that, use it.
| Good fit | Not a fit |
|---|---|
| builds, tests, renders, model runs: work that ends | long-running services |
| sending work to machines you own, on any network | an interactive shell, or arbitrary commands |
| a few to many workers, each offering what it is good at | scheduling on its own: call submit from your cron |
| each job in its own clean folder | splitting one job across many nodes |
From nothing to the first result
Install it on both sides:
brew install diphyx/tap/grid There is also a release archive, and Docker images: ghcr.io/diphyx/grid-hub and ghcr.io/diphyx/grid-node.
Each side makes its own key pair. Then copy hub.pub to the node and node.pub to the hub. A .key never leaves its machine.
grid key new /etc/grid/hub # on the hub grid key new /etc/grid/node # on the node The node says what it can do, and which hub it trusts:
# /etc/grid/node.toml grid = 2 identity = "/etc/grid/node.key" tags = ["build"] [[hubs]] ip = "10.0.0.1" trust = "/etc/grid/hub.pub" [cmds.build] exec = ["make", "-j8"] The hub knows its nodes:
# /etc/grid/hub.toml grid = 2 identity = "/etc/grid/hub.key" [[nodes]] tags = ["build"] ip = "10.0.0.2" trust = "/etc/grid/node.pub" Open TCP 9420 on the node to the hub's IP. Start both, then send the job from the hub:
grid node run # on the node grid hub run # on the hub grid hub submit build.toml --wait # follow it; exits 0 only if it succeeds grid hub pull <id> ./result # copy the results out grid hub tail <id> shows everything the job did, as it happens:
19:34:29.631 1 job queued 19:34:29.633 3 hub attempt 1 on 10.0.0.2:9420 19:34:29.640 6 cmd running build 19:34:29.651 9 out built linux 19:34:29.660 11 cmd done build 19:34:29.663 13 job done 🔌 Want your own hub or node, in any language? The protocol is written down byte for byte in spec/SPEC.md, with a test kit. If yours passes the kit, it works with every other GRID hub and node.
Give it a try
If you have ever typed scp, ssh, scp one more time and thought "there must be a better way", that is exactly what GRID is for.
The Get Started page goes from nothing to a first job's results in six steps. If it saves you time, share it with your team, your lab, or that colleague who still has an old ~/work folder on every box.






