The Belfort ML client
The Belfort ML client is the end client's side of Belfort and holds the secret
key. belfort-ml-client ships it as the belfort-ml-client command and the
belfort_ml_client.Client class. Your end clients run it on their own machine,
on the client token you mint with model.create_client. It talks to the
gateway itself and never receives an API key.
sequenceDiagram
participant C as belfort-ml-client
participant G as Gateway
participant S as Storage
C->>G: GET /client/code
G-->>C: download links, one per file, and their digest
C->>S: download the client C++
Note over C: check the digest, build the client, generate a key pair
C->>G: POST /client/keys
G-->>C: upload links, one per evaluation key, each bound to its size
C->>S: upload the evaluation keys
C->>G: POST /client/keys/verify
loop until a deploy holds the keys
C->>G: GET /client/status
end
Note over C: encrypt the input
C->>G: POST /client/infer
G-->>C: encrypted answer
Note over C: decrypt
opt without BELFORT_CLIENT_SECRET_SEED
C->>G: DELETE /client
end
Running it#
belfort-ml-client infer --token "$BELFORT_CLIENT_TOKEN" --input input.json| Flag | Environment | Default | What |
|---|---|---|---|
--token |
BELFORT_CLIENT_TOKEN |
required | The client token from the provider |
--cache-dir |
BELFORT_CLIENT_CACHE |
$XDG_CACHE_HOME/belfort-ml-client, or ~/.cache/belfort-ml-client |
Where the linked client code is kept, per model and compile |
--input |
required | A JSON file with the input, or - for stdin |
|
--timeout |
3600 s | How long to wait for a deploy to load the keys | |
BELFORT_CLIENT_SECRET_SEED |
none | A 32-byte seed (64 hex characters) the keys derive from; a later run with the same seed resumes the key set. Without it, the run releases the key set at exit |
The client downloads only its own code: the generated client code and the runtime headers it compiles. It never receives the model weights or the server code.
The input file holds {"values": [...]}: the model input flattened in
row-major order, with as many entries as the example input you compiled with.
For a model with several arguments, it holds {"inputs": [[...], [...]]} with
one list per argument; only one of them may be encrypted. The decrypted answer
goes to stdout as JSON, flattened the same way:
{"values": [...], "request_id": "req_...", "latency_ms": 847, "encrypt_ms": 12, "decrypt_ms": 3}latency_ms is the time the gateway's call to the GPU runtime took: the
ciphertext transfers between the gateway and the runtime, the wait in the
runtime, and the GPU computation. It does not include the transfers between
this client and the gateway, or the gateway's own work. A replayed request
reports the time of its first run. encrypt_ms and decrypt_ms are the time
this client took to encrypt the input and to decrypt the answer.
Progress goes to stderr. On failure the command prints
belfort-ml-client: <message> and exits with status 1.
The machine needs a C++20 compiler to build the client code for each model.
The build runs clang++; set CXX to use another compiler, such as g++. The
platform wheels carry the Cyclops client library
(Installation). No GPU is needed.
What one run does#
GET /client/codenames the model's compile, one presigned download URL per file, and the digest the gateway verified. The client downloads the files, checks them against the digest, and links the client binary. The linked code is cached per model and compile, so the next run on the same compile skips the download and the build.- The client binary generates the key pair and keeps the secret key in its own
memory.
POST /client/keysnames the evaluation keys and returns one presigned upload URL per key;POST /client/keys/verifychecks they all arrived. The gateway takes at most 1000 keys, each at most 1 GiB (1,073,741,824 bytes) and 64 GiB in all. GET /client/statusreportsawaiting_keys,awaiting_upload,no_deploy,loading,readyorfailed. Once the keys are verified, the gateway loads them onto the deploy you started for the key set.no_deploykeeps the client waiting, since you may start the deploy after handing over the token;failedends the run. It means every deploy of the key set failed to load the keys three times, the newest deploy of the key set failed to start, or verify found a key of the wrong size.POST /client/infersends one ciphertext and returns one. A request that could not connect is sent again. One whose answer was lost after sending raisesinference_outcome_unknown, since the inference may have run.
A token opens one key set, and its keys can be named once. The secret key ends
with the process, so the next run needs a new token from you, unless both runs
set the same BELFORT_CLIENT_SECRET_SEED.
To run belfort-ml-client infer more than once on a token, generate a seed
before the first run and set it on every run:
export BELFORT_CLIENT_SECRET_SEED=$(openssl rand -hex 32)The seed is the end client's secret key: anyone who holds it can decrypt the end client's data. Keep it where the end client keeps other secrets, never send it to the provider or to Belfort, and use a new one with each new token.
From Python#
belfort_ml_client.Client does the same in steps, and runs as many inferences
as needed on one key pair while its process lives:
from belfort_ml_client import Client
with Client(token) as client: # or BELFORT_CLIENT_TOKEN
client.prepare() # code, keys, and a deploy holding them
answer = client.infer([values]) # one list per model argumentLeaving the with block, or close(), stops the key-pair process and keeps
the key set. Client(token, secret_seed=client.secret_seed) in a later process
restores the key pair and skips the key upload; the Quickstart's client.py
keeps the seed in a file per token for this. release() ends the client at
the gateway: DELETE /client unloads its key set and deletes its keys, which
ends the token. The deploy serves no other key set, so stop it. Otherwise the
gateway deletes the key material once the token expires (see
Deploys and credits).
If the deploy holding the keys stops while a Client runs, infer() waits for
the gateway to load them onto the next ready deploy you start for the key set,
for up to an hour, and then sends the same ciphertext again, at most three times per
infer(). It does so only when the gateway answered that no deploy holds
the keys before the request could run. If an earlier attempt lost its answer,
the inference may have run on the deploy that stopped, and infer() raises
ClientError with code inference_outcome_unknown instead of running it a
second time. It raises the same after a 5xx answer to the inference, which can
mean the GPU ran it and the gateway lost the result.
| Method | What it does |
|---|---|
Client(token=, base_url=, secret_seed=, cache_dir=, http_transport=) |
secret_seed resumes this token's key set; reads BELFORT_CLIENT_TOKEN and BELFORT_CLIENT_CACHE for what is not passed, and calls the gateway the token names unless base_url is passed; http_transport replaces httpx's transport for the gateway calls |
prepare(timeout=3600) |
fetch_code(), publish_keys(), _warm_up() then wait_until_ready() |
fetch_code() |
GET /client/code, download, check the digest, link; or reuse the cached link |
publish_keys() |
Generate the key pair, POST /client/keys, upload, POST /client/keys/verify |
status() |
GET /client/status |
wait_until_ready(timeout=3600) |
Poll status() until ready; raise ClientError on failed or the timeout |
secret_seed |
The seed the secret key derives from, once the keys exist |
release() |
DELETE /client: end the key set and the token |
close() |
Stop the key-pair process; the key set stays |
infer(arguments) |
Encrypt one input, POST /client/infer, decrypt; returns the dict the command prints |
A refused or failed gateway call, a failed presigned transfer, a digest
mismatch and a timeout raise belfort_ml_client.ClientError, whose message says
what to do next. A gateway that stays unreachable after the retries raises
httpx.TransportError, except during infer(), which raises ClientError
with code inference_outcome_unknown. A failed link or client binary raises RuntimeError
(or OSError when it cannot start). A Client is not safe to share between threads. Clients on
the same compile share its cached code; one that has to link it again, after an
upgrade of belfort-ml-client or the Cyclops library, waits until the others
close.
Retries#
The client makes up to 4 attempts at each gateway call and each presigned
transfer. It retries a dropped connection and the statuses 429, 500, 502, 503
and 504, waiting 1, 2, then 4 seconds, or what Retry-After asks for, at most
30 seconds. A presigned transfer is also sent again on a 400 RequestTimeout,
which the bucket answers when an upload stalls for about 20 seconds. It also waits out 409 request_in_progress and verify_in_progress,
which mean an earlier attempt of the same call is still running: a verify for
up to 10 minutes, an inference for up to 420 seconds. Releasing waits up to 10
minutes for a key load in flight. An inference is sent again only after a
connection that failed before sending, or a 429; after a 5xx or a lost answer
it raises inference_outcome_unknown, since the GPU may have run it.
Timing#
Building the client code, generating keys and uploading the evaluation keys each take minutes for a large model, and the upload is bounded by the end client's bandwidth.