better/compute

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.

Requirements

Node.js 22.12 or newer; 22.18 or newer to load a TypeScript configuration such as better-compute init writes, because Node.js strips types by default from 22.18. No database server is needed.

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/pglite

With 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.0

This 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 8
  • dev runs the API and every worker role on 127.0.0.1:3000, and always migrates first.
  • --memory uses a throwaway in-memory PGlite. Leave it out to keep the data between runs, in .better-compute/dev-db.
  • --sim 8 adds 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 l40s gives 4×L40S machines instead; hgx-h100-gpu-missing gives machines that fail acceptance and are never adopted; cpu gives 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) adopted

Behind 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 help lists them), and they read the tenant from --tenant or BETTER_COMPUTE_TENANT. See command groups.
  • api GROUP.METHOD key=value … calls any method; key:=json passes JSON values. api --list prints every route with the IAM action it is decided on.
  • nodes list shows eight nodes with provisionState adopted.

Copy the pool id and look at its capacity:

export POOL=pool_…   # the id pools list printed
better-compute capacity --pool $POOL

The 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 1h

reserve 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 demo

A 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 $POOL

release 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:

reserve.ts
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
});

See the TypeScript client.

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:

better-compute.config.ts
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 4

dev 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

Was this page helpful?

Better Compute is created by Sean Filimon

Last updated

On this page