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.