Execute code on a remote JupyterHub kernel from any terminal — zero dependencies.
Project description
jupyterhub-exec
Execute code on a remote JupyterHub kernel from any terminal.
Born from an AI agent farm, solving a real infrastructure problem. Execute code on a remote JupyterHub kernel from any terminal — zero external dependencies.
pip install jupyterhub-exec
Why
JupyterHub provides GPU compute. Your agent terminal does not.
jh-exec bridges the two using the Jupyter kernel protocol over a raw WebSocket —
no browser, no notebook UI, no library dependencies beyond the Python standard library.
┌─────────────────────────┐ WebSocket ┌──────────────────────────┐
│ Agent Terminal (CPU) │ ───────────────────────► │ JupyterHub Kernel (GPU) │
│ Claude Code / CLI │ ◄─────────────────────── │ PyTorch / CUDA │
└─────────────────────────┘ stdout stream └──────────────────────────┘
Usage
# Execute a script on the remote GPU kernel
jh-exec run train.py
# Execute inline code
jh-exec exec "import torch; print(torch.cuda.is_available())"
# List running kernels
jh-exec kernels
# Start a new kernel
jh-exec new-kernel
Configuration
Set via environment variables or a .env file in the working or home directory:
Public JupyterHub (HTTPS — default):
JH_HOST=hub.example.com
JH_PORT=443
JH_USER=agent-01
JH_TOKEN=your_token_here
JH_TIMEOUT=600
Local JupyterHub (HTTP):
JH_HOST=192.168.1.100
JH_PORT=8000
JH_USER=agent-01
JH_TOKEN=your_token_here
JH_SSL=false
JH_TIMEOUT=600
Or pass directly:
jh-exec --host hub.example.com --port 443 --ssl --user agent-01 --token your_token run script.py
Python API
from jh_exec import execute, list_kernels, new_kernel
# Execute code, stream output to stdout
execute("import torch; print(torch.cuda.get_device_name(0))")
# List running kernels
kernels = list_kernels()
# Start a new kernel, get its ID
kid = new_kernel()
Dedicated GPU per agent
In jupyterhub_config.py:
def assign_gpu(spawner):
gpu_map = {
"agent-01": "0",
"agent-02": "1",
"agent-03": "2",
}
spawner.environment["CUDA_VISIBLE_DEVICES"] = gpu_map.get(spawner.user.name, "")
c.Spawner.pre_spawn_hook = assign_gpu
Benchmark
Validated on NVIDIA GeForce GTX TITAN X via gpu_demo.py:
Visible GPUs: 4 torch 2.5.1+cu121
[0] NVIDIA GeForce GTX TITAN X (11.9 GiB)
[1] NVIDIA GeForce GTX TITAN X (11.9 GiB)
[2] NVIDIA GeForce GTX TITAN X (11.9 GiB)
[3] NVIDIA GeForce GTX TITAN X (11.9 GiB)
8192x8192 matmul: 233.2 ms (4.7 TFLOP/s)
checksum: 862523.7500
allocated: 776 MiB
CPU vs GPU — same script, offloaded (ml_demo.py)
The same MLP-training script, run locally on CPU vs offloaded to the remote GPU kernel — only the command changes.
Local, on CPU:
$ python3 ml_demo.py
====================================================
Device : CPU
torch : 2.11.0+cu130
====================================================
step 1/30 loss 1.0095
step 10/30 loss 0.9030
step 20/30 loss 0.7048
step 30/30 loss 0.5562
----------------------------------------------------
30 steps in 35.45s (0.8 steps/s)
====================================================
>>> ran on CPU in 35.45s (0.8 steps/s)
Offloaded to the remote GPU kernel:
$ jh-exec run ml_demo.py
====================================================
Device : NVIDIA GeForce GTX TITAN X
torch : 2.5.1+cu121
====================================================
step 1/30 loss 1.0114
step 10/30 loss 0.8923
step 20/30 loss 0.6796
step 30/30 loss 0.4982
----------------------------------------------------
30 steps in 0.86s (34.9 steps/s)
GPU memory used: 1008 MiB
====================================================
>>> ran on NVIDIA GeForce GTX TITAN X in 0.86s (34.9 steps/s)
~41× faster, zero code change, zero local GPU — a CPU-only terminal offloading real training to a remote GPU it doesn't have.
Full GPU offload from a Claude Code terminal — zero local GPU, zero dependencies.
The hard way — the same run without jh-exec
To run that exact ml_demo.py on the remote GPU without this package, you drop down to
the raw JupyterHub + Jupyter kernel protocol yourself. the_hard_way.py
does precisely that, and it's what jh-exec run ml_demo.py replaces:
- a third-party dependency —
websocket-client(the stdlib has no WebSocket client); - the JupyterHub REST kernel lifecycle —
POSTto spawn a kernel,DELETEto clean it up (or leak a GPU kernel every run); - the Jupyter v5 messaging protocol — build an
execute_request, filteriopubbyparent_header.msg_id, stop onstatus: idle; - config plumbing — parse
~/.env, handleJH_HOSTcarrying its own port, pickhttp/wsvshttps/wss.
Same result, ~90 lines and a dependency. jh-exec run ml_demo.py is the whole of it —
transparent: the script is byte-for-byte identical, and nothing about the remoting leaks
into your code.
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file jupyterhub_exec-0.1.4.tar.gz.
File metadata
- Download URL: jupyterhub_exec-0.1.4.tar.gz
- Upload date:
- Size: 10.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a0208c91c95481606897a5a973785747f9c9e33763e03fa130bd44f2f6036915
|
|
| MD5 |
b39083e08bb81b60bc0f52d34695cb1d
|
|
| BLAKE2b-256 |
843a6a65e4d20616fdce16559f41b8c171adfb78796aa8242cd21ecfb45356ca
|
File details
Details for the file jupyterhub_exec-0.1.4-py3-none-any.whl.
File metadata
- Download URL: jupyterhub_exec-0.1.4-py3-none-any.whl
- Upload date:
- Size: 9.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5eebfc76e0c3b8a4fbce5f6d140ca6805c83fe7d9e5b7fcf3bde62948cc80d28
|
|
| MD5 |
ca6aff7d635f95efc36c644fb7d4a49f
|
|
| BLAKE2b-256 |
d68f921ed94728dafa57c14b158b6cfb3cadbaf631b3885b214268692ac3548c
|