Skip to content

Operate a model server

loom-serve separates process lifetime, model registration, request admission and parameter residency. A model can stay registered while drained or cold; changing one of these states need not restart the server or discard other models. The first-output walkthrough establishes a running server and a vectors deployment for the commands below.

Find the state you need

curl --fail --silent --show-error http://127.0.0.1:8080/v1/status
curl --fail --silent --show-error http://127.0.0.1:8080/v1/models

/v1/status includes every registration and the server's actual resources. /v1/models lists prepared deployments and their capabilities; a prepared but drained model remains listed. /readyz describes the server, not readiness of every model. The status fields answer different questions:

Field on each model Question
loom.registration Is this registration declared, preparing, ready, retiring, failed or awaiting a release retry?
loom.status.admission Is the gate open, draining, drained or removing?
loom.status.phase What is the modality scheduler doing?
loom.parameter_operation Is an explicit activation/deactivation waiting or in progress?
loom.parameters What reloadable weight plans are active and how much retained backing do they account for?
loom.variants Which native executable choice is preferred, and how many choices are retained?
loom.error Did preparation or a lifecycle operation fail?

The registration wire values are registered, preparing, ready, retiring, failed and release_failed. Scheduler phases are modality-specific and are not measurements of device utilization.

Register without immediately consuming model memory

A model document with "admission":"drained" registers metadata without reading programs or weights or allocating model storage. It still occupies one registration slot and has an observation stream. This is useful for a catalog larger than the set of models that can be resident simultaneously.

The normal default is "open". Registration returns HTTP 202 after accepting server-owned intent. Preparation and JIT happen asynchronously. A client disconnect does not cancel that registration; /v1/status reports its outcome. A failed source package remains inspectable without stopping healthy peers. After correcting its inputs, unregistering and registering again starts an explicit new attempt with a new incarnation.

The model-document reference applies equally to startup --model arguments and HTTP registration. HTTP changes live state; the client owns persistence of those changes into its launch configuration.

Drain, resume or remove

curl --fail --silent --show-error http://127.0.0.1:8080/v1/models/drain \
  -H 'Content-Type: application/json' -d '{"model":"vectors"}'
curl --fail --silent --show-error http://127.0.0.1:8080/v1/models/undrain \
  -H 'Content-Type: application/json' -d '{"model":"vectors"}'

Drain closes admission when the owner processes the request. Accepted model work, including queued requests, continues. New inference, tokenizer, checkpoint pinning, and checkpoint suspension requests wait outside the model within the holding budget. Checkpoint deletion remains available so clients can release retained history while inference is drained. Control and observation routes remain available. A successful drain response means the gate changed; admission:"drained" confirms accepted work has finished.

Drain is reversible and does not evict weights, erase sessions or remove the registration. Undrain reopens admission; held requests enter that modality's normal bounded admission policy. Repeated drain/undrain is idempotent.

Unregister is irreversible:

curl --fail --silent --show-error http://127.0.0.1:8080/v1/models/unregister \
  -H 'Content-Type: application/json' -d '{"model":"vectors","mode":"drain"}'

mode:"drain" is the default and lets accepted requests finish. mode:"abort" cancels their intent. Both wait for actual CPU/device borrowers to retire before releasing memory, and return HTTP 202 for accepted removal. Graceful removal can be escalated to abort but cannot be undone by undrain.

The name stays occupied during retirement; a duplicate registration returns 409. The model leaves /v1/models as soon as removal is accepted, but remains in /v1/status until retirement finishes and its name is released. Absence from /v1/status confirms completion. With concurrent administrators, compare loom.incarnation: the same name may already identify a new registration.

Held arrivals receive 410 during removal, and requests for the absent name receive 404 after retirement. A quiescent platform release failure remains visible in /v1/status as release_failed, retaining the resources needed to retry. Another unregister explicitly retries that release.

Control weight residency independently

Explicit activation requires a healthy registration with admission closed:

curl --fail --silent --show-error http://127.0.0.1:8080/v1/models/drain \
  -H 'Content-Type: application/json' -d '{"model":"vectors"}'
curl --fail --silent --show-error http://127.0.0.1:8080/v1/models/activate \
  -H 'Content-Type: application/json' -d '{"model":"vectors"}'
curl --fail --silent --show-error http://127.0.0.1:8080/v1/status

This prepares and warms the primary computation without opening admission. It leaves a text deployment's optional image projector dormant. HTTP 202 accepts intent; loom.parameter_operation advances through waiting and activating to idle. Check loom.error and wait for idle before undrain. loom.parameters.primary.active alone means weight activations have been enqueued, not that device work has completed.

For a text deployment with image input, explicit projector warmup uses:

curl --fail --silent --show-error http://127.0.0.1:8080/v1/models/activate \
  -H 'Content-Type: application/json' -d '{"model":"scout","component":"image"}'

The registration must be drained first. This prepares the projector lazily, then warms its parameters. loom.parameter_component identifies the current or last operation; check loom.parameters.image.error as well as loom.error when it becomes idle. A projector source/preparation error remains local to image input; ordinary text can still run after undrain.

With elastic backing, /v1/models/deactivate releases reloadable weights from a healthy, drained, unpinned model while retaining compiled programs, sessions and checkpoints. It accepts the same {"model":"name"} body and returns 202. Wait for parameter_operation:"idle", check loom.error, and inspect loom.parameters.primary.active and committed_bytes. Fixed primary backing cannot deactivate and returns 409; a metadata-only drained registration is already cold. With component:"image", deactivation releases projector weights on elastic backing or retires the projector on fixed backing. Both preserve text histories and checkpoints. Fresh image demand reloads weights or prepares the retired projector; a retained image-prefix hit needs neither.

Native weight allocation and release run independently of the serving scheduler: another ready model can continue serving during either transition. Retained memory reports pending_bytes for accepted allocations and releasing_bytes for releases still in flight. Releasing bytes remain included in committed_bytes until native cleanup completes; they are not free capacity. Requests waiting for that capacity resume from its completion notification.

Undrain and conflicting residency operations return 409 while an operation is pending. Ordinary inference can reload evicted weights on demand without re-JIT. Successful activation does not itself pin weights against later pressure.

/v1/models/pin and /v1/models/unpin accept the same optional component selector and change only that component's eviction eligibility. Pinning is not activation: a cold component remembers the policy without loading its weights. Status exposes it as loom.parameters.primary.pinned or loom.parameters.image.pinned. A component with pending deactivation cannot be pinned. Unpinning never removes protection held by accepted work.

Warmup releases its temporary protection when complete; it does not create a persistent pin. Pinning the primary language model can prevent its projector from fitting on a constrained device, even when the projector is unpinned. Pin only components that must remain resident within the deployment's budget.

Return idle backing without removing models

Automatic reclamation is opt-in. For example, this server releases unused command workspace after 30 seconds and unpinned weights after five minutes:

loom-serve --device=amdgpu --model=/srv/models/scout/model.json \
  --workspace_idle_ms=30000 --parameter_idle_ms=300000

Each duration is independent; 0 disables that policy. Workspace expiry applies to each device's shared command pool after its last allocation actually retires. Weight expiry starts when a model's last invocation pin or explicit parameter pin is released. Polling status, receiving telemetry, and leaving a client connected do not keep idle resources hot. Explicitly pinned weights and active invocations remain protected. Fixed-backed weights cannot deactivate; workspace expiry works with either backing policy.

Expiry queues native release without draining the server or waiting on unrelated requests. The next invocation reloads weights and reuses the compiled programs. Registrations, sessions, KV/prefix history, and named checkpoints stay intact: these policies release reloadable backing, not conversation history. They therefore need not reduce an idle process to its minimum possible footprint.

/v1/status publishes the configured durations in server.idle.workspace_ms and server.idle.parameter_ms. Device workspace commitment and per-model parameter activity/accounting show reclamation and subsequent reloads. Expiry checks use the smaller enabled duration, capped at one second; physical release finishes asynchronously. When nothing awaits retirement or expiry, maintenance stops rearming its timer. Heartbeat reporting has an independent interval and can be disabled without disabling reclamation.

Replace native code without reloading weights

A prepared text deployment can JIT replacement command programs and kernels while keeping its weights, control VM, tokenizer, sessions and checkpoints. Staging and selection are separate: several candidates can compile without changing inference. Explicit selection makes a ready choice preferred at a completed execution boundary. There is no unregister/register cycle or required admission drain.

The author asserts that replacements preserve the meaning and layout of retained state and parameters. The server checks the native binding contract, not mathematical equivalence. Preparation declarations, shape catalog, control VM and tokenizer remain fixed; changing them requires a new registration. For a text deployment named scout with variant capacity enabled, these commands use jq to carry server-issued IDs:

original=$(curl --fail --silent --show-error \
  http://127.0.0.1:8080/v1/models/variants -H 'X-Loom-Model: scout' |
  jq -er '.selected')

candidate=$(curl --fail-with-body --silent --show-error \
  http://127.0.0.1:8080/v1/models/variants \
  -H 'Content-Type: application/json' \
  -d "{\"model\":\"scout\",\"base\":$original,\"base_directory\":\"/srv/models/scout/candidate\",\"sources\":[\"config.loom\",\"advance.loom\"]}" |
  jq -er 'select(.variant.phase == "ready") | .variant.id')

curl --fail-with-body --silent --show-error \
  http://127.0.0.1:8080/v1/models/variants/select \
  -H 'Content-Type: application/json' \
  -d "{\"model\":\"scout\",\"variant\":$candidate,\"expected\":$original}"

These JSON-only commands use provider files already on the server. A multipart source package instead carries client-local providers in the request. Staging waits asynchronously by default and returns retained compiler diagnostics on failure; "wait":false instead returns an ID immediately for later inspection. Existing inference can progress during JIT. Selection briefly quiesces that model's accepted CPU/device work before rebinding native stages; it neither reloads weights nor copies KV state. Subsequent text epochs use the new code, including epochs advancing already-admitted requests. This is a model-wide preference, not a request-local A/B selector.

Keeping the original choice makes rollback another selection:

curl --fail-with-body --silent --show-error \
  http://127.0.0.1:8080/v1/models/variants/select \
  -H 'Content-Type: application/json' \
  -d "{\"model\":\"scout\",\"variant\":$original,\"expected\":$candidate}"

Expected-current protection prevents another author's update from being silently overwritten. After an interrupted response, inventory establishes the actual preference; disconnect does not cancel accepted work. Rollback restores code, not a conversation's earlier state. Named checkpoints provide independent state forks when comparing continuations. Native-update compatibility must hold for every retained session and checkpoint that either choice will consume.

Unused terminal candidates can be explicitly removed to reclaim cache credit. Pending and failed attempts count against variant_capacity; capacity exhaustion rejects new staging instead of evicting an arbitrary rollback choice. The HTTP reference defines inspection, selection and removal responses. The live development workflow combines these controls with remote source uploads, checked kernel changes and pinned continuation trials.

Bound memory and admission

Process limits and model capacities constrain different resources:

Limit Resource it bounds
--connections HTTP connections shared by all listeners.
--held_requests Requests waiting outside closed/unprepared model gates.
--model_capacity All registered names, including failed and retiring models.
Model pending_requests Modality request credit; text/image count waiting requests, finite services include the active cohort.
Text rows Concurrently advancing execution rows.
Text context_capacity Logical token ceiling of one context.
Text pool_capacity Shared logical KV capacity, independent of maximum context times row count.
Text variant_capacity Native choices, including the original, pending JITs and failed attempts.
--memory_bytes Shared physical parameter/state/workspace backing under elastic policy on one device.

Holding capacity stays below connection capacity so held inference cannot consume every control connection. Live log streams and waiting variant-management requests share an additional combined bound with held requests. Full credit returns 429. Incomplete requests and active response streams still consume transport credit; these bounds are not a promise that arbitrary slow clients leave connections free.

Text admission reserves a request's completion high-water. Short contexts grow by blocks rather than eagerly reserving each row's maximum physical slab. When an oldest request cannot fit, admission_overtakes permits a bounded number of smaller fitting requests to pass; its default is the execution-row count. Zero selects FIFO. This bounds successful overtakes, not elapsed waiting time: a slow active reader can retain its reservation.

Elastic parameters, mutable state, invocation input/output, and command scratch share one physical pool. Admission uses the model's prepared working-set peak and outstanding work, rather than probing free HAL memory. Command scratch can be reused after its ordered release; an escaping output remains charged until both its producer and final consumer retire, even if its model is removed.

Elastic pressure first reclaims idle workspace, then idle unpinned weights and eligible idle text history across models sharing the same device pool. Accepted work, prepared continuations, and explicit checkpoint references remain protected. An evicted session can replay its full input history; a session name is not durable storage. Pool pressure does not automatically serialize conversations to disk. If all capacity is protected and no accepted work can release it, admission reports overload rather than reclaiming live storage.

Trim without changing the budget

curl --fail --silent --show-error http://127.0.0.1:8080/v1/devices/trim \
  -H 'Content-Type: application/json' -d '{"device":0,"target_bytes":0}'

This requests reduction of eligible serving-managed commitment toward the target. It releases idle workspace and unpinned reloadable parameters, not active work or retained conversations. HTTP 200 returns current accounting, including accepted native releases that have not completed. /v1/status reports subsequent completion; protected storage can prevent reaching the target. Fixed weights are not reclaimable. Trim does not change the configured limit or discard compiled code.

The observation workflow explains shared backing and its workspace subset. Adding those counters double-counts workspace; neither is total VRAM or process RAM. Reported free bytes are not a guarantee that the next request can be admitted.

Shut down safely

SIGINT/SIGTERM stops application admission and joins accepted asynchronous work before destruction. Cancellation does not make an already-submitted device operation disappear. There is no timeout that authorizes freeing live buffers.

Invalid requests and isolated source failures stay scoped to their requests or registrations. A native failure affecting shared backing instead stops admission and drains accepted work before reporting the terminal error. Completion callbacks publish outcomes; they do not tear down shared devices or pools.

HTTP and source loading assume a trusted deployment. TCP/Unix listeners share the same authority; socket permissions, tunnels or an external proxy supply access control. Command options describe binding and resource configuration.