Quickstart
Install Better Compute from npm, run the development server with simulated GPU machines, reserve GPUs, watch capacity change and release them.
This walkthrough runs a complete control plane on your laptop: PGlite as the database, an embedded Better IAM, a
gateway, and eight simulated HGX H100 machines that join the way the real daemon does. You then reserve four GPUs,
see where they were placed, watch capacity change, and release them. Nothing above the daemon is mocked: the
machines get sealed join tokens, call the Join RPC, open a Session stream, pass diag.nvml acceptance and are
adopted by the real runtime.
Install
In a new, empty project, install Better Compute, Better IAM (for the embedded IAM that --sim starts) and PGlite
(the in-memory database):
npm install better-compute better-iam @electric-sql/pgliteWith pnpm, also list @better-iam/server, @better-iam/core and @better-iam/adapter-sqlite; see
Installation. Check the command:
npx better-compute version # 0.1.0This page writes the command as better-compute: in the project directory, run it as npx better-compute (or
pnpm exec better-compute), or define an alias such as alias better-compute="npx better-compute".
better-compute help lists every command, and better-compute help dev shows one. The
CLI reference has them all.
Start the development server with simulated machines
npx better-compute dev --memory --sim 8devruns the API and every worker role on127.0.0.1:3000, and always migrates first.--memoryuses a throwaway in-memory PGlite. Leave it out to keep the data between runs, in.better-compute/dev-db.--sim 8adds a gateway on loopback (cleartext HTTP/2, sessions authenticated by a header, for local development only) and joins eight simulated machines through it.--sim-variant l40sgives 4×L40S machines instead;hgx-h100-gpu-missinggives machines that fail acceptance and are never adopted;cpugives machines without GPUs. The simulated machines come from@better-compute/testing, which the umbrella package depends on.
With no configuration file, dev runs the server's own groups and the built-in inventory and capacity modules,
with a development secret generated once in .better-compute/dev-secret. No IAM is wired in, so --sim starts an
embedded, in-memory Better IAM and prints a root administrator's token for it. Standard error shows:
Development database: pglite:memory (secret in …/.better-compute/dev-secret)
better-compute listening on http://127.0.0.1:3000/api/compute
Workers running: controllers, scheduler, sinks, iam
Dev gateway (h2c, header auth, loopback only): http://127.0.0.1:52011
Embedded IAM root tenant …; root token (dev only): biam_ses_…
Joined 8 simulated hgx-h100 node(s) into pool_…
8 simulated node(s) adoptedBehind those lines, dev created a shared in region dev-1, a metal-host
for H100 GPUs whose acceptance policy runs diag.nvml and expects 8 GPUs, and one
with maxUses 8. Each simulated machine joined with it, received a node
certificate, opened its session, passed acceptance, and became adopted at desired-state generation 1, which is when
the placer starts to consider it. Leave this terminal running.
Look around
Open a second terminal in the same directory (in a clone, define the alias again), and act as the root administrator
that dev printed. With
BETTER_COMPUTE_URL set, the CLI calls the running server over HTTP instead of loading a configuration:
export BETTER_COMPUTE_URL=http://127.0.0.1:3000
export BETTER_COMPUTE_TOKEN='biam_ses_…' # the root token dev printed
export BETTER_COMPUTE_TENANT='…' # the root tenant dev printed
better-compute api system.whoami
better-compute nodes list
better-compute pools list --query "items[].id"
source <(better-compute completion bash) # optional: tab completion for every command- Every API group has its commands (
nodes list,pools list,reserve, ...;better-compute helplists them), and they read the tenant from--tenantorBETTER_COMPUTE_TENANT. See command groups. api GROUP.METHOD key=value …calls any method;key:=jsonpasses JSON values.api --listprints every route with the IAM action it is decided on.nodes listshows eight nodes withprovisionStateadopted.
Copy the pool id and look at its capacity:
export POOL=pool_… # the id pools list printed
better-compute capacity --pool $POOLThe rollup reports, per resource, total, allocatable, allocated, reserved and available, plus the GPUs
that are provisioning, unhealthy, in maintenance or unreachable. Eight adopted H100 boards give 64 allocatable
GPUs.
Reserve GPUs
better-compute reserve --pool $POOL --gpus 4 --for 1hreserve calls reservations.create. The request goes through the full
admission sequence: the tenant pre-check, an idempotency lookup, a draft reservation, exactly one Better IAM decision
on compute:reservations:create, then placement in a second transaction. The CLI prints the idempotency key it
generated on standard error; repeat the command with --key and the same value to retry safely without booking twice.
The pool's owner gets an effectively unlimited GPU quota when the pool is created, so this reservation fits. To
try a cap, set one with better-compute quotas set --pool $POOL --resource gpu --max 4
and reserve again: the second request fails with QUOTA_EXCEEDED. See
quotas and usage.
With the default inline placer, the answer is already placed: the placer picked one node, locked it and its GPUs,
and inserted four bookings. The reservation then moves on without you:
Read it back, with the id reserve printed:
better-compute reservations get rsv_… --query state # active
better-compute reservations get rsv_… --query binding{
"nodeId": "node_…",
"cdiDevices": ["bcd.dev/gpu=GPU-…", "bcd.dev/gpu=GPU-…", "bcd.dev/gpu=GPU-…", "bcd.dev/gpu=GPU-…"],
"envFile": "/run/bcd/allocations/alloc_….env"
}The binding is a hold: the node's desired state gained a device assignment, and the (simulated) daemon wrote a CDI
spec for exactly the four booked GPUs and reported them. A workload manager on the node, such as Slurm, Kubernetes or
containerd with CDI, consumes those device names. Run better-compute capacity --pool $POOL again: allocated is 4
and available is 60.
Watch events
Events leave through a transactional outbox. The /watch route of the repository's examples/dev configuration
(see with the repository's example configuration) streams
your tenant's events as server-sent events, and events watch prints them one JSON line each. A dev without a
configuration has no /watch route, so skip this step there:
better-compute events watch --types "node.*,reservation.*"It is plain server-sent events, so curl -N -H "Authorization: Bearer $BETTER_COMPUTE_TOKEN" "$BETTER_COMPUTE_URL/watch?types=node.*" shows the same. In another terminal, cordon a node and watch
node.cordoned arrive:
better-compute nodes cordon node_… --reason demoA cordoned node keeps its current reservations; the placer just stops choosing it. nodes uncordon reverses it.
Release
better-compute release rsv_…
better-compute capacity --pool $POOLrelease calls reservations.release. The bookings are truncated to
the current database time, the device assignment leaves the node's desired state, and available returns to 64.
Billing covers reserved time, from active until the release. A reservation that has not started yet is withdrawn
with release --cancel instead.
The same calls over HTTP and TypeScript
Every CLI command above is a call to the HTTP API, POST /api/compute/{group}/{method}. Requests need a JSON body,
content-type: application/json and X-Better-Compute: 1; answers are { "data": … } or
{ "error": { "code", "message" } }:
curl -s http://127.0.0.1:3000/api/compute/pools/list \
-H "Authorization: Bearer $BETTER_COMPUTE_TOKEN" \
-H "content-type: application/json" -H "x-better-compute: 1" \
-d "{\"tenantId\": \"$BETTER_COMPUTE_TENANT\"}"In TypeScript, the client is typed by your server instance through a type-only import, so every group, method, input and result is checked without code generation:
import { createComputeClient } from 'better-compute/client';
import type { compute } from './compute.server'; // type-only: your betterCompute() instance
const client = createComputeClient<typeof compute>({
baseURL: 'http://127.0.0.1:3000',
token: () => process.env.BETTER_COMPUTE_TOKEN,
});
const reservation = await client.reservations.create({
tenantId,
poolId,
devices: [{ class: 'gpu', count: 4 }],
duration: 3_600_000, // milliseconds in the TypeScript type; over HTTP "1h" works too
binding: 'hold',
idempotencyKey: 'quickstart-1', // retrying with the same key returns the same reservation
});Your own configuration
better-compute init writes a starter better-compute.config.ts in the current directory, and never overwrites an
existing one. This is what it writes, with its comments shortened:
import { unconfiguredIam, type CommandSpec } from 'better-compute/cli';
import { pglite } from 'better-compute/pglite';
import { postgres } from 'better-compute/postgres';
import { defineConfig } from 'better-compute/server';
export default defineConfig(({ env = process.env, cwd = process.cwd() } = {}) => ({
database: env.BETTER_COMPUTE_DATABASE_URL
? postgres({ url: env.BETTER_COMPUTE_DATABASE_URL })
: pglite({ dataDir: `${cwd}/.better-compute-pglite` }),
secret: env.BETTER_COMPUTE_SECRET ?? '',
baseURL: env.BETTER_COMPUTE_BASE_URL ?? 'http://localhost:3000',
rootTenantId: 'root',
iam: unconfiguredIam(),
}));
export const commands: CommandSpec<any>[] = [];It imports the umbrella package's subpaths, so run it in a project where better-compute resolves, with the
optional peers @electric-sql/pglite (for PGlite) and @better-iam/server, @better-iam/core and
@better-iam/adapter-sqlite (for the embedded Better IAM of --sim, all three in better-iam) installed: the
project from the first step is one. See Installation. The template reads the secret
from the environment, and betterCompute() refuses one shorter than 32 characters with INVALID_CONFIG:
better-compute init
export BETTER_COMPUTE_SECRET=$(node -e "console.log(require('node:crypto').randomBytes(32).toString('base64url'))")
better-compute dev --sim 4dev finds the configuration by walking up from the working directory. unconfiguredIam() authenticates nobody,
so --sim again starts the embedded Better IAM; once you wire a real one in, dev --sim acts as
BETTER_COMPUTE_TOKEN, which must then be a root administrator's token.
Next steps
How the control plane, the gateway, the daemon and Better IAM fit together.
Joining nodesJoin tokens, the install script and what happens when a real machine joins.
ReservationsDurations, leases, extensions, future starts and the reservation state machine.
Local developmentThe dev server, the deterministic simulator and the example deployments.
Better Compute is created by Sean Filimon
Last updated
Introduction
Better Compute is an embeddable TypeScript control plane for GPU, CPU and sandbox capacity that uses Better IAM for identity, policy and billing.
Installation
Requirements, installing better-compute and Better IAM from npm, the umbrella package's subpaths, packages outside it, optional peers, and building from a clone.