GPUlayer: One Layer Between Your Code and Every GPU You Can Reach Developer Francesco Antonello released GPUlayer, an Apache 2.0-licensed tool published on PyPI that adds a scheduling and control layer between code and GPUs spread across laptops, servers, Kubernetes pools and cloud VMs, letting a single `gpulayer submit` command replace manual ssh, rsync and tmux workflows. GPUlayer routes jobs through one queue and capability matcher across four workload types, supports Python functions via a `@gl.remote` decorator with `.map()` fan-out, and is validated on Apple Silicon, NVIDIA CUDA (Tesla T4), automatic Google Cloud GPU provisioning and fleets of up to 30 machines across two cloud regions. Antonello said he built it partly so coding agents such as Claude Code and MCP-enabled editors could inspect and submit work to GPUs without being handed raw SSH keys. GPUlayer: One Layer Between Your Code and Every GPU You Can Reach Most GPU software assumes the GPU is attached to the machine where you type python . PyTorch, Lightning, Hugging Face Accelerate, JAX, MLX, and CuPy all do a great job deciding how a computation uses the hardware it is given. None of them decide which machine should run the job, ship your project and inputs there, check whether the driver and libraries actually work before starting, or bring logs and checkpoints back when it finishes. I built GPUlayer https://www.gpulayer.dev/ GitHub https://github.com/antonellof/gpulayer to solve two closely related problems: 1. My GPUs are spread across different machines : an Apple Silicon laptop, Linux servers, and rented cloud VMs. Every time I started a new workload, I ended up repeating the same manual ritual— ssh , rsync , hoping the virtualenv and CUDA build still matched the driver, starting tmux , and copying checkpoints back. 2. I needed a way to give my coding agents control over all of my GPUs : I do most of my day-to-day work inside coding agents—Claude Code, Pi agents, and MCP-enabled editors—running on my laptop. Handing an agent raw SSH keys and teaching it which box is free, which one has CUDA vs. Metal mps / mlx , and how to rsync artifacts back is fragile and dangerous. I wanted one structured control layer so an agent on my MacBook can inspect every GPU I can reach, submit a training run or benchmark to the right machine, stream the logs, and pull back the results. Before GPUlayer, running a job on another machine looked like this: ssh gpu-box rsync -av my project/ gpu-box:~/my project/ hope the virtualenv and CUDA build still match the driver tmux new -s train cd ~/my project && python train.py check back later, rsync checkpoints home Multiply that by several machines with different drivers, a Mac with Metal mps and mlx instead of CUDA, a 96-run learning-rate sweep, or a multi-node job where torchrun needs matching ranks, addresses, and ports on every host. With GPUlayer, that becomes: pip install gpulayer gpulayer submit --accelerator cuda --source my project -- python train.py GPUlayer is published on PyPI under Apache 2.0, and validated on Apple Silicon, NVIDIA CUDA Tesla T4 , automatic Google Cloud GPU provisioning, and fleets of up to 30 machines across two cloud regions. This post walks through what it does, how the scheduler and distributed runtime work, how it performs against PyTorch Lightning and torchrun , and where its boundaries are today. What GPUlayer does GPUlayer sits underneath your frameworks and above your machines: your laptop, servers that joined with gpulayer join , Kubernetes worker pools, or cloud VMs provisioned on demand. Everything goes through the same queue, capability matcher, and wire protocol across four workload types: 1. Any command or binary Scripts, shell pipelines, C++ or Rust binaries, Blender renders, or ffmpeg transcodes run as jobs without wrapper code. You point GPUlayer at a source directory, declare inputs and outputs, and state the hardware requirements: Run a script on any worker with a CUDA GPU gpulayer submit --accelerator cuda --source ./app -- ./render.sh Stage an input file and run a shell job gpulayer submit --source examples/shell job \ --input examples/shell job/sales.csv:sales.csv -- bash report.sh 2. Python functions with any library Decorate a function with @gl.remote , specify the accelerator and framework versions it needs, and call it directly or fan it out across every free worker with .map : python import gpulayer as gl @gl.remote accelerator="mps", frameworks={"mlx": " =0.10"} def matmul benchmark n: int - dict: import mlx.core as mx ... Scatters across available workers in parallel results = matmul benchmark.map 1024, 2048, 4096 Arguments and return values are plain JSON, and code travels as a clean source bundle rather than brittle pickled bytecode. 3. PyTorch training and distributed launch GPUlayer includes a focused PyTorch integration gpulayer.pytorch with a Trainer , Runtime , and launch . The exact same script runs in-process on your laptop, as multi-process DDP on one machine, or across multiple nodes: Single process python examples/trainer regression/train.py 2-process DDP on CPU python examples/trainer regression/train.py --accelerator cpu --devices 2 2 nodes x 2 processes across remote workers python examples/trainer regression/train.py --accelerator cpu --nodes 2 --devices 2 --target remote Or directly in Python: python import gpulayer.pytorch as glt trainer = glt.Trainer accelerator="cuda", devices=4, num nodes=2, max epochs=10, target="remote", trainer.fit model, train loader, val loader The Trainer handles device placement, mixed precision fp16 / bf16 , torch.compile , gradient accumulation with DDP no sync , sanity validation steps, callbacks EarlyStopping , ModelCheckpoint save top k=N , LearningRateMonitor , test , predict , CUDA side-stream batch prefetching, two-level gradient sync for multi-node CPU/gloo runs, and experimental FSDP2 strategy="fsdp" . If you already use PyTorch Lightning or Accelerate, glt.launch places the job and starts the processes with the standard torchrun environment variables they expect. 4. Long-running services For inference endpoints or persistent workers, gpulayer submit --service or gl.service ... runs replicas on distinct machines, assigns each replica a free $PORT , waits for it to accept connections, reports host:port endpoints via Job.endpoints , restarts crashed replicas with exponential backoff, and re-places replicas if a worker machine disappears. Placement that explains itself When a job fails on a cluster, the worst outcome is waiting three minutes for container setup and import torch only to hit RuntimeError: No CUDA GPUs are available or a missing library. In GPUlayer, workers probe their hardware and environment on startup. For GPU libraries torch.cuda , jax.cuda , cupy.cuda , torch.mps , mlx , GPUlayer does not merely check whether the Python package is importable—it runs a lightweight GPU probe in a separate interpreter to verify that the library can actually execute on the device. A CPU-only jax or broken CUDA installation will never advertise itself as a working GPU worker. When a job arrives, the scheduler evaluates every worker against the job’s requirements. If no worker qualifies, the job is rejected immediately with per-worker reasons: scheduling: no eligible worker gpu-box-1 needs 2 usable cuda device s , worker has 1 mac-studio framework 'jax' not installed laptop torch on this worker cannot use CUDA gpu-box-2 eligible Topology-aware groups and NVLink/PCIe GPU selection Placement goes beyond counting free GPUs: - Rack, zone, and region locality --group-locality : Workers report their cloud region , zone read automatically from the GCE metadata server on Google Cloud or passed via flags/env , and an optional gpulayer.dev/network-group label for rack or InfiniBand islands. When gang-scheduling a multi-node job, the default prefer policy keeps all nodes in the same network group if possible, then the same zone, then the same region, before falling back to cross-region placement. - Best-connected GPUs on a host : On multi-GPU machines, workers parse nvidia-smi topo -m into link speeds and NUMA affinities. When a task asks for multiple GPUs on an 8-GPU server, GPUlayer picks the free subset whose slowest pairwise interconnect is fastest preferring an NVLink island or GPUs behind a single PCIe switch over arbitrary lowest indices and exports CUDA DEVICE ORDER=PCI BUS ID so mixed-GPU machines never mismatch device indices. From one laptop to cloud GPUs, Kubernetes, and AI agents Adding machines in one command For local work, the control plane runs embedded inside your Python process or CLI call with zero setup. When you want to pool multiple machines, start the server on one host: gpulayer up gpulayer up generates a TLS certificate with a pinned fingerprint, creates named role-based tokens admin , submitter , join , metrics , and prints the exact gpulayer join command to paste on any other machine: gpulayer join https://192.168.1.50:8443 --token