Skip to main content
levers9 cluster join attaches a cluster you already run, GKE, EKS, AKS or your own, as a pool of GPU workers. It works the way levers9 login works for a person: the cluster side initiates everything. One agent runs inside the cluster, holds an outbound stream to the controlplane, reports the nodes and what they have free, and starts worker Jobs when asked. The workers carry their own inbound connections back over the same kind of stream. So there is no public node address, no firewall rule, no allow-list of controlplane addresses, and the controlplane holds no credentials for your cluster.

The shape

Both arrows are opened from inside your cluster and stay open; nothing on the controlplane side ever dials the cluster or its nodes. That is the whole difference from a configured pool:

What the cluster needs

Three things, and only the first involves your cloud’s network settings: If you cannot reach the API server from your own machine, run levers9 cluster join … --manifest-only and apply the manifest from wherever you do have access, such as a bastion or Cloud Shell; the join completes when the agent dials in.
Upgrading from a configured pool? The controlplane egress addresses that pool required on your authorized networks are no longer used by anything and can come off the list once the agent is up.

Join

Point your kube context at the cluster, then:
What it does, and prints as it goes:
  1. Registers the cluster with the controlplane, which mints a token for the agent.
  2. Applies one manifest through your kube context: the namespace, a service account with the least access the agent needs, the token as a secret, and the agent Deployment.
  3. Waits until the agent has dialed in and reported its nodes, and prints them.
From then on a job from your workspace that asks for that GPU type can land on the cluster:
Options that matter: The same flow exists on the dashboard’s Clusters page: register there, then apply the manifest it shows, or run the printed command.

Who a cluster serves

A cluster you attach works for your workspace and nobody else’s. Your jobs land there by GPU type, with no pool selector, and no other workspace’s job is ever sent to it. Its workers carry a token that’s only good for your workspace, and the settings they get hold none of the platform’s own credentials: images come through the controlplane, which hands the worker a short-lived link for each image your job uses. Secrets and the LEVERS9_TOKEN your code sees are fetched for each container as it starts, the way they are on a private pool, and that token is a restricted one. A shared cluster works for every workspace, the way the platform’s own GPU cluster does: any job asking for its card can land there. Only a platform admin can share a cluster, and only a platform admin can detach a shared one:
levers9 cluster list shows your own clusters and the shared ones, with a Serves column. A shared cluster someone else attached shows its card, its state and what each node has free, but not its node names, namespace, image or manifest. Taking a shared cluster back takes effect with the next job. Sharing one takes effect as new workers start there. Either way, workers already running keep the token and settings they started with until they wind down, so delete their Jobs in the cluster if that can’t wait. A workspace can attach up to five clusters (worker.attachedClusters.quotaPerWorkspace in the controlplane config; 0 means no limit). Attaching and detaching need your own token: a container’s token can’t do either.

Keeping the agent current

The worker image is pushed to your cluster’s registry by the same build that pushes ours, and each worker Job pulls the tag fresh, so workers are never behind. The agent is a Deployment and stays on the image it started with until something restarts it:
Nothing in flight is lost — the agent only launches and stops worker Jobs, and the ones already launched keep running while it restarts. Do it after an upgrade, or whenever levers9 cluster list shows an agent that has been up far longer than the platform’s last release.

What runs in the cluster

Exactly what the manifest contains, nothing more: The agent only ever creates Jobs in its own namespace and refuses orders for any other. The worker tunnel only ever reaches the worker’s own containers and refuses any other target. Neither can be used as a way into the cluster. Check it yourself against the service account, with kubectl auth can-i --as: Reading pods cluster-wide is what makes shared nodes work: the agent reports the cards other tenants leave free, so the scheduler never sends a job to a node whose GPUs are already taken. What the controlplane learns about your cluster is exactly the nodes the selector names: their names, their cards, and their free CPU and memory. With no selector that is every node in the cluster, node pools you never meant to lend included, so name them with --selector if that matters to you.

Who talks to whom

The agent and the workers never talk to each other. Each has its own line to the controlplane and nothing else; the picture is a hub with spokes, and the hub is the controlplane. The agent reads the worker Jobs’ outcome only to answer the controlplane; once a Job exists the agent is out of the picture for it. If the agent pod dies, running workers keep running and keep serving jobs; what stops is new placement, because the nodes’ last report goes stale after ninety seconds. Kubernetes restarts the agent Deployment on its own, so that gap is short. If a worker dies, its Job ends and the controlplane asks the agent for a new one when the next job needs the node.

Failure modes, and what covers each

How the nodes are sized

Every ten seconds the agent reports each node’s allocatable CPU, memory and GPUs, and what is free net of the other pods on it. A node shared with another workload offers only what that workload leaves; a node with no free GPU is not offered at all until one frees up. A worker takes every card the node has free when it is created and serves several jobs across them. The controlplane always counts its own workers on a node, pending or running, before adding another, so a node whose worker was created when fewer cards were free keeps that size until the worker winds down (a few idle minutes after its last job); the next worker then takes everything free. A job asking for several cards needs one worker with that many, so it waits for a node with that many free at once. Two ceilings hold regardless of what the reports say: a node never gets a second worker while one of its workers is still pending (its pod has not started, so the node report cannot see it), and a cluster pool never carries more pending workers than it has nodes. The agent enforces the same ceiling on its side (no more pending worker pods than nodes) and removes a worker Job whose pod has not started within twenty minutes. Above all pools, attached or not, the scheduler holds a request when a pool already has twenty workers pending or received more than thirty in a minute (worker.provisioningGuard in the controlplane config; a negative value disables a check). A bug in the accounting above them can cost at most one idle worker per node, never a flood. How a job uses several cards, one training across them or many trainings side by side, is on Train on 1, 2, 4 or 8 cards.

Disk on the nodes

Workers keep the images they pull in a cache on the node, at /var/lib/levers9/images/cache, so the next worker there starts without pulling them again. It outlives every worker, so every worker keeps it in check:
  • it’s pruned to 100 GB, the images used longest ago going first;
  • anything nobody has used for seven days is removed, once an hour, even when there’s room;
  • when less than 20% of the node’s disk is free, it gives up what it can until 25% is, whatever its size.
An image that a running worker on the node has mounted is never removed, and neither is anything used in the last ten minutes. The 20% is there because the kubelet’s defaults evict pods at 10% free on the node’s disk and at 15% free on the disk that holds the container images, which on GKE is the same disk. The worker is one of those pods, and so is the job running in it. What the cache can’t free is the jobs’ own disk: a container’s scratch space and anything it writes. If a node still reports DiskPressure, the worker’s log says what is left is mounted or in use, and the fix is a bigger disk or a smaller job. The limits are worker.nodeCache in the controlplane’s config (imageCacheMaxGB, minFreePct); the controlplane hands them to the workers it starts on your cluster.

Day two

  • See it. levers9 cluster list for your clusters and the shared ones, levers9 cluster nodes <name> for a cluster’s nodes and free cards; the dashboard’s Clusters page shows the same, live.
  • Share it. levers9 cluster share <name> (platform admins) lets every workspace use it; --off gives it back to the workspace that attached it.
  • Host network. Workers on attached clusters use the node’s network, as they always have. A platform admin can turn that off with worker.attachedClusters.hostNetwork: false: the workers then run on the pod network and reach the controlplane through their tunnel. Try it on one cluster first.
  • A new worker image. Point the cluster at it with --worker-image at join, or edit the Deployment’s image; the agent creates workers from the image the controlplane names in each Job.
  • Re-apply the agent. levers9 cluster manifest <name> | kubectl apply -f - reinstalls everything but the token, which stays in the secret already there.
  • Leave. levers9 cluster leave <name> revokes the token, forgets the pool, and deletes the agent’s objects from the cluster. A shared cluster can only be detached by a platform admin, since other workspaces’ jobs run there. Joining again under the same name gives you a cluster that isn’t shared.

Compared with a configured pool

A pool with provider: kubernetes in the controlplane’s values (see GPUs in a GKE cluster) has the controlplane call the cluster’s API itself, with a credential you give it, and dial each worker. It still works and needs nothing inside the cluster, at the price of public nodes, a firewall rule and allow-lists that follow the controlplane’s addresses. Joining with the agent is the default for anything outside the controlplane’s own network.