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.

A job file goes into the hub. The hub picks a node and sends it the job and files. The node runs it and sends output and results back.

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.

node.toml offers build as make -j8; a job asks for build with args linux; what runs is make -j8 linux.

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

Node ALPHA is lost mid-run: its job goes back to queued, waits for Node BETA, and finishes there.

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.

The job's three parts: push ./src brings the files into the space, the build step runs make -j8 linux on them, pull out brings out/ back.

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 ./src right 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.

Job 1 sends its shared file once and Node ALPHA keeps it. Job 3 has the same shared file, so only its other files are sent.

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.

GitHub · Docs · Built by DiPhyx