docs.

run gpu workloads on remote gpus without installing drivers or a cuda toolkit. your cuda calls are routed to a cloud gpu; your code stays where it is.

run from vs code

Attach a cloud gpu to GitHub Codespaces, a dev container, a remote VPS, WSL, or your local workspace. Open a Python file and run it with Lupine from the editor toolbar while your code and Python environment stay where they are.

install from the vs marketplace →
  1. 1. installAdd the extension to the machine that owns your VS Code workspace.
  2. 2. openOpen a saved Python file. Lupine uses your selected interpreter or uv project.
  3. 3. runClick the Lupine play action. The extension handles CLI setup and browser sign-in.

set up with an AI agent

Tell your agent to follow the setup skill. It will install the Lupine CLI, walk you through sign-in, and verify that this machine can see a GPU.

Follow https://console.lupine.sh/skill.md to set up Lupine and verify GPU access on this machine.
view skill →

manual setup

Install the CLI, log in, and run a pytorch script on a remote gpu. No drivers, no cuda toolkit, no code changes.

1. install the cli

curl -fsSL https://dl.lupine.sh/install.sh | sh

installs to /usr/local/bin, or ~/.local/bin if that isn't writable.

2. log in

lupine login

opens your browser to sign in with github or google. credentials are stored in ~/.lupine.

linux

3. watch a gpt learn to write, no files of your own

no pip install, no venv, nothing to write to disk. a tiny gpt — Karpathy's nanoGPT model, trained on an embedded public-domain poem instead of a downloaded dataset — run straight from its url. watch it go from random characters to (most of) the actual poem in a few hundred steps.

lupine run -- \
  uv run --with torch \
  https://console.lupine.sh/gpt_bench.py
view gpt_bench.py →

lupine run leases a gpu and preloads its shims, so the script's own device="cuda" resolves to it — no lupine-specific code.

macos

3. install pytorch and the adapter

pip install lupine torch

the lupine package is a thin adapter — it hands pytorch a device backed by a remote gpu. a cpu-only torch build is fine.

you also need a container runtime: apple container, docker, podman, or nerdctl — whichever is installed.

4. write a payload and run it

train.py — wrap the gpu work in connect().

import lupine
import torch

with lupine.connect() as gpu:
    device = gpu.device()

    x = torch.randn(4096, 4096, device=device)
    print((x @ x).sum().item())
lupine run python train.py

lupine run leases a gpu and holds it for the life of the command. connect() picks up that lease and returns a device — no host or credentials to pass.

macos has no cuda userspace, so connect() starts a pytorch sidecar container to hold the cuda side. your script, your venv, and your data stay on your machine — only cuda calls leave it.

macos & the sidecar

macOS has no CUDA driver, so the CLI can't preload the gpu shims the way it does on linux. Instead lupine run executes your command inside a small linux sidecar container that carries the shims, so your cuda calls still reach the leased remote gpu. Your command runs the same way it would locally, just one layer down.

The first time you run without choosing a mode, it asks before doing anything:

macOS has no local CUDA driver. Run inside a Docker
container so CUDA reaches the leased GPU? [Y/n]

Accept and it pulls the sidecar image once, then runs. You need a container runtime installed and running (Docker or Podman). Choose the mode explicitly to skip the prompt:

--container docker
run inside a docker container. this is what the prompt does when you accept.
--container podman
use podman instead of docker.
--container none
skip the sidecar and run directly on your machine. on macOS this means no remote gpu (there's no local cuda driver), so it's mainly an escape hatch.

files

Your current directory is mounted into the container at /workspace and is the working directory, so relative paths just work. Anything your command writes into it is saved back to your machine, which is how model checkpoints and other outputs come back. Files outside your current directory (like ../../train.py or an absolute path) aren't visible inside the container, and the CLI warns you when it spots one. cd into the directory you want and run from there. Your login (~/.lupine) is mounted too, so you stay signed in.

the image

The sidecar is ghcr.io/lupinemachines/sidecar, pulled automatically and pinned to your CLI version so the container's shims always match the CLI that launched it. Point LUPINE_CONTAINER_IMAGE at a different tag to override it.

Using the lupine python adapter with connect() instead (see the macos setup above)? That path manages its own sidecar from inside python. This section is about running an ordinary command through lupine run.

cli reference

every command takes --api, --config, --org, and -v/--verbose. run lupine <command> --help for the authoritative list.

running workloads

lupine run [args...]
run a command with cuda calls routed to a remote gpu. leases a gpu and holds it for the life of the command.
lupine run -- cmd --flag
pass -- first when your command takes its own flags, otherwise the CLI parses them.
lupine run -c "cmd"
a shell command string instead of positional args.
lupine shell
an interactive shell with remote gpu access. takes --gpu, --region, and --container.
--local
default. runs on your machine and forwards cuda over the network.
--remote
runs in-region on a runner pod co-located with the gpu. no local shim or sidecar involved.
--container docker|podman|none
macOS only: run the command in a linux sidecar container so cuda reaches the gpu. defaults to a prompt. see macos & the sidecar.
--gpu <sku>
request a specific gpu type — see lupine gpus.
--region <region>
pin a region. without it, regions are ranked by latency from your machine.
--pip <pkg>
install a package in the remote env. repeatable, cached by content.
-r, --requirements <file>
install a requirements.txt in the remote env, cached by content.
-o, --output-dir <dir>
where to write files the run produced. defaults to the current directory.
--no-artifacts
don't download files the run produced.

runs and leases

lupine ps
list recent runs.
lupine artifacts <run>
list the files a run produced, or download them with -o <dir>.
lupine cancel <run>
cancel a run.
lupine end
release the cached lease now instead of waiting for the idle timeout. a lease is reused across runs and billed per second while a workload is attached.

services

lupine deploy -f lupine.yaml
create or update a managed, autoscaling service from a spec. re-running it updates the service in place. -f defaults to lupine.yaml.
--image <ref>
override the spec's image, typically a digest pushed by CI.
--env KEY=VALUE
set an env var over the spec's env, so secrets stay out of the file. repeatable.
--gpu <sku>, --name <name>
override the spec's gpu or service name.
lupine deploy list
list your services.
lupine deploy status <id>
a service, its replicas, and its endpoint.
lupine deploy stop <id>
release the service's gpus but keep the record.
lupine deploy start <id>
bring a stopped service back, re-provisioning its warm pool.
lupine deploy delete <id>
delete a service and release its gpus.
lupine registry login <registry>
store credentials so deploys can pull private images. use -u/-p, or --token (the username defaults to x-access-token). credentials belong to the org and are stored encrypted.
lupine registry list
list stored registries.
lupine registry logout <registry>
remove a registry's stored credentials.

account and capacity

lupine login
sign in via the browser with github or google. --external prints the sign-in url for another app to open; --check exits successfully when you're already signed in.
lupine logout
remove locally stored credentials.
lupine gpus
available gpu types with vram and hourly pricing. the sku column is what --gpu takes.
lupine regions
selectable regions, their status, and the gpu types offered in each. feeds --region.
lupine status
the configured api and the gpu instances it can see.
lupine usage
month-to-date gpu time and estimated cost.
lupine billing
billing status and where to manage payment methods.
lupine invoices
your invoice history.
lupine feedback <message>
send feedback to the lupine team. requires a login.
lupine version
the installed CLI version.
lupine upgrade
replace the installed binary with the latest release, without re-running the install script.

orgs

lupine org
list your orgs, with * marking your default. orgs own services, registry credentials, ci trusts, and billing. every command acts in your default org unless you pass --org or set LUPINE_ORG.
lupine org create <slug>
create an org; you become its admin. the slug is permanent: services are served at <name>.<slug>.lupine.sh. --name sets a display name.
lupine org switch <slug>
make an org your default.
lupine org members
list the org's members.
lupine org invite <email>
invite someone by email (admins). --role member|admin, default member.
lupine org role <email> <admin|member>
change a member's role (admins).
lupine org remove <email>
remove a member (admins). anyone can remove themselves.
lupine org invites
invites waiting for you.
lupine org accept|decline <org>
answer an invite.

ci and ssh access

lupine oidc add <owner/repo>
let a repository's github actions jobs authenticate without a stored secret. narrow it with exactly one of --ref, --pull-request, or --environment; --workflow further limits a --ref trust to one workflow file. see github actions.
lupine oidc list
repositories your orgs trust.
lupine oidc revoke <repo-or-id>
stop trusting a repository and revoke the tokens it issued, so running jobs lose access.
lupine ssh-key add [path]
add a public key for the lupine ssh gateway. defaults to the first of ~/.ssh/id_ed25519.pub, id_ecdsa.pub, and id_rsa.pub; pass - to read stdin. --title labels it.
lupine ssh-key list
your ssh keys.
lupine ssh-key remove <id|fingerprint>
remove a key and end the sessions it opened.

configuration

lupine config set api <url>
point the CLI at a coordinator. stored in ~/.lupine/config.json.
lupine config get api
read the configured coordinator.
LUPINE_API_URL
coordinator url, overriding the config file.
LUPINE_ORG
org slug to act in when --org isn't passed.
LUPINE_TOKEN
bearer token used instead of your lupine login credentials, for automation where a browser sign-in isn't possible.
LUPINE_STATE_DIR
where the config and your credentials live, overriding ~/.lupine. point it at a mounted volume to keep a container logged in across runs.
LUPINE_DISABLE_LOCAL
set to 1 to route every cuda call to the leased gpu. by default a machine with its own driver keeps its local gpus in the device list alongside the remote ones, so device="cuda" may resolve to hardware you already have. any value except 0, false, or no counts as set.
LUPINE_LIB_PATH
directory holding the gpu shims, overriding the bundled ones. needed on linux/arm64, where releases ship no shims.
LUPINE_SERVER, LUPINE_SESSION
gateway and lease, set automatically by lupine run for the command it runs. the adapter reads them, so lupine.connect() needs no arguments.

examples

end-to-end guides that take a workload from an empty directory to a running service. each one was run against production as written.

serve an llm with vllm

deploy Qwen/Qwen2.5-0.5B-Instruct behind vLLM's OpenAI-compatible api as a lupine service, then send it a chat completion. the model is small and ungated, so it runs on the cheapest gpu and needs no Hugging Face token.

1. prerequisites

sign in, and make sure the CLI is v0.4.10 or newer (older versions reject the scale-to-zero warming strategy below). lupine deploy builds the image on your machine, so you also need Docker with Buildx.

lupine login
lupine version   # v0.4.10 or newer; otherwise run: lupine upgrade
docker buildx version

2. the image

in an empty directory, save this as Dockerfile. it is a thin layer on the public vLLM image.

FROM vllm/vllm-openai:v0.11.0

# Bake the weights into the image so replicas start without downloading them.
ENV MODEL=Qwen/Qwen2.5-0.5B-Instruct \
    HF_HOME=/models
RUN huggingface-cli download "$MODEL" && \
    test -n "$(ls -A /models/hub)"
ENV HF_HUB_OFFLINE=1

EXPOSE 8000
ENTRYPOINT ["sh", "-c", "exec vllm serve \"$MODEL\" --served-model-name qwen --host 0.0.0.0 --port 8000 --enforce-eager"]
  • weights baked in: the huggingface-cli download runs at build time, so a new replica starts serving without downloading the model first. the test fails the build if nothing was downloaded.
  • HF_HUB_OFFLINE=1: set after the download, so at runtime vLLM loads the baked weights and never reaches out to the Hugging Face hub.
  • --served-model-name qwen: requests name the model qwen instead of the full repo id, so swapping the model later doesn't change your clients.
  • --enforce-eager: skips CUDA graph capture, which shortens startup at a small cost in throughput. drop it once startup time matters less than tokens per second.

3. the service spec

save this next to it as lupine.yaml. build: . builds the Dockerfile in this directory, so you don't need a registry.

name: vllm-qwen
build: .
gpu: a100-40gb
region: us-east-2
ports: [8000]
scaling:
  min: 1
  max: 1
  warming:
    strategy: scale-to-zero

scaling.min: 1 is required: without a scaling block a service has no minimum replica, nothing starts, and the endpoint answers 404 page not found. the scale-to-zero warming strategy lets an idle replica release its gpu where the gpu pool supports it, but don't count on it: plan as if the service holds an a100 for as long as it runs.

4. deploy

lupine deploy -f lupine.yaml

the first deploy builds the image with Buildx and uploads it, over 10 GB, almost all of it the vLLM base image, so expect it to take several minutes. the CLI prints the service's id and endpoint when it's done.

5. send a request

the replica is warming while it gets a gpu and loads the model. check the service until the replica is ready. a status of no_capacity means no a100 is free yet; lupine keeps retrying on its own.

lupine deploy status <id>

set ENDPOINT to that url, which is https://vllm-qwen.<your-org>.lupine.page, and list the models (the responses below are trimmed):

ENDPOINT=https://vllm-qwen.<your-org>.lupine.page
curl -s $ENDPOINT/v1/models
{"object":"list","data":[{"id":"qwen","object":"model","owned_by":"vllm",
"root":"Qwen/Qwen2.5-0.5B-Instruct","max_model_len":32768, ...}]}

then ask it something:

curl -s $ENDPOINT/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model": "qwen", "messages": [{"role": "user", "content": "What is the capital of France?"}]}'
{"id":"chatcmpl-...","object":"chat.completion","model":"qwen",
"choices":[{"index":0,"message":{"role":"assistant",
"content":"The capital of France is Paris.", ...},"finish_reason":"stop"}],
"usage":{"prompt_tokens":36,"total_tokens":44,"completion_tokens":8}, ...}

the api is OpenAI-compatible, so any OpenAI client works with its base url set to $ENDPOINT/v1 and the model set to qwen.

6. use a different model

change MODEL in the Dockerfile and run lupine deploy -f lupine.yaml again; the service updates in place and keeps serving it as qwen. the weights and their activations have to fit in the gpu's memory, so a larger model may need a larger gpu in lupine.yaml: lupine gpus lists each type with its vram and hourly price. a gated model also needs a Hugging Face token at build time.

7. clean up

a running service holds its gpu and is billed per second, idle or not, at the a100's hourly rate from lupine gpus. when you're done, stop it to release the gpu and keep the service, or delete it:

lupine deploy stop <id>     # release the gpu, keep the service
lupine deploy delete <id>   # remove it entirely

billing

add a card before running GPU workloads. you're only charged for what you use. manage your card and view invoices on the billing page.

usage & credit: GPU time is metered per second and charged to your card as your usage adds up — there's no subscription. once you've paid your first invoice we add a one-time $20 credit to your account, applied automatically to future usage until it runs out. the credit comes after your first payment rather than at signup, which keeps throwaway accounts from farming free GPU.

card verification: when you add a card we place a temporary $1 authorization hold to confirm it's valid and can actually be charged. it's applied to your first charge, or released within 7 days if you don't use it, so you never pay more than your usage. you'll see a $1 pending charge until then (your bank may take a few days to clear it after release). a card that declines the check (which is common with prepaid or virtual cards) isn't accepted.

a saved card alone doesn't prove it can pay and verifying up front keeps fraudulent signups off shared GPU capacity so real customers can get GPUs.

questions? sign in to see your account, or read more at lupine.sh.