AMD Inference Microservice deployment guide#
This guide gives step-by-step instructions to deploy the AMD Inference Microservice (AIM) container for the dauparas/proteinmpnn model in different environments. Follow these instructions to run the model on AMD accelerators. This guide assumes a MI300X accelerator on the target system.
This AIM has an API of its own, shaped by what the model takes and returns. See Testing your deployment for the endpoints and payloads of this model.
Prerequisites#
AMD Instinct™ GPU with ROCm support (e.g., MI300X, MI325X)
Docker installed and configured with GPU support
A host directory for the model cache, if the model downloads its weights at first start
1. Docker deployment#
1.1 Running the container#
Set AIM_PROFILE_ID to one of the profiles listed in the README. The runtime then uses that profile
and does not select a profile from the detected hardware.
mkdir -p /path/to/model-cache
docker run \
-e AIM_ID=dauparas/proteinmpnn \
-e AIM_PROFILE_ID=bentoml-mi300x-fp32-tp1-throughput \
-v /path/to/model-cache:/workspace/model-cache \
--device=/dev/kfd --device=/dev/dri \
--group-add video --ipc=host \
-p 8000:8000 \
amdenterpriseai/aim-instinct-model-dauparas-proteinmpnn:2026.24.0-preview
1.2 Customizing deployment with environment variables#
Customize your deployment with optional environment variables:
docker run \
-e AIM_ID=dauparas/proteinmpnn \
-e AIM_PROFILE_ID=bentoml-mi300x-fp32-tp1-throughput \
-e AIM_CACHE_PATH=/workspace/model-cache \
-e AIM_LOG_LEVEL=INFO \
-e AIM_PORT=8080 \
-v /path/to/model-cache:/workspace/model-cache \
--device=/dev/kfd --device=/dev/dri \
--group-add video --ipc=host \
-p 8080:8080 \
amdenterpriseai/aim-instinct-model-dauparas-proteinmpnn:2026.24.0-preview
Without AIM_PROFILE_ID, the runtime selects a profile from the detected hardware.
AIM_ACCELERATOR_COUNT changes the accelerator count that the selection uses. The runtime does not use it when
AIM_PROFILE_ID is set.
2. Model caching for production#
This AIM has no separate model download step. If the model downloads its weights, it does so at service
startup and keeps them in $AIM_CACHE_PATH, which defaults to /workspace/model-cache.
Mount a persistent volume at that path, as shown in section 1.1. Without the mount, the container downloads the weights again at each start.
If the cache must be at a different path, set AIM_CACHE_PATH and mount the volume at the same path:
docker run \
-e AIM_ID=dauparas/proteinmpnn \
-e AIM_PROFILE_ID=bentoml-mi300x-fp32-tp1-throughput \
-e AIM_CACHE_PATH=/mnt/aim-cache \
-v /path/to/model-cache:/mnt/aim-cache \
--device=/dev/kfd --device=/dev/dri \
--group-add video --ipc=host \
-p 8000:8000 \
amdenterpriseai/aim-instinct-model-dauparas-proteinmpnn:2026.24.0-preview
The first start is slower than later starts, because it downloads the weights. Size the volume for the model checkpoint, and keep it writable.
3. Kubernetes deployment#
3.1 Deployment#
It is possible to deploy AIM with Kubernetes. This section gives a sample Kubernetes deployment manifest.
Create deployment.yaml with the following content:
apiVersion: apps/v1
kind: Deployment
metadata:
name: minimal-aim-deployment
labels:
app: minimal-aim-deployment
spec:
progressDeadlineSeconds: 3600
replicas: 1
selector:
matchLabels:
app: minimal-aim-deployment
template:
metadata:
labels:
app: minimal-aim-deployment
spec:
containers:
- name: minimal-aim-deployment
image: amdenterpriseai/aim-instinct-model-dauparas-proteinmpnn:2026.24.0-preview
imagePullPolicy: Always
env:
- name: AIM_ID
value: "dauparas/proteinmpnn"
- name: AIM_PROFILE_ID
value: "bentoml-mi300x-fp32-tp1-throughput"
- name: AIM_CACHE_PATH
value: "/workspace/model-cache"
- name: AIM_LOG_LEVEL_ROOT
value: "INFO"
- name: AIM_LOG_LEVEL
value: "INFO"
- name: AIM_PORT
value: "8000"
ports:
- name: http
containerPort: 8000
resources:
requests:
memory: "64Gi"
cpu: "8"
amd.com/gpu: "1"
limits:
memory: "64Gi"
cpu: "8"
amd.com/gpu: "1"
startupProbe:
httpGet:
path: /healthz
port: http
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 360
readinessProbe:
httpGet:
path: /readyz
port: http
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet:
path: /healthz
port: http
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 30
volumeMounts:
- name: model-cache
mountPath: /workspace/model-cache
- name: ephemeral-storage
mountPath: /tmp
- name: dshm
mountPath: /dev/shm
volumes:
- name: model-cache
persistentVolumeClaim:
claimName: minimal-aim-model-cache
- name: ephemeral-storage
emptyDir:
sizeLimit: 256Gi
- name: dshm
emptyDir:
medium: Memory
sizeLimit: 32Gi
The startup probe gives the start up to one hour: 360 checks at 10-second intervals. That covers a first start that downloads weights, as described in section 2. The pod stays out of the service endpoints until this probe succeeds, so no request reaches a model that is still loading. A later start reads the cache and succeeds much sooner.
The readiness probe checks /readyz every 10 seconds. After three failed checks (30 seconds) Kubernetes takes the
pod out of the service endpoints, so a worker that fails after a successful start stops receiving requests until it
recovers. The liveness probe below is slower on purpose and only restarts the container.
Kubernetes runs the liveness probe only after the startup probe succeeds. The liveness probe then accepts 30 failed checks, which is five minutes, before it restarts the container. This tolerates a model that does more work after the health endpoint answers.
The memory and cpu values above are a starting point. Raise them if the profile uses more than one accelerator,
because this AIM starts one worker process per accelerator, and each worker holds its own copy of
the model.
Create the cache volume claim in pvc.yaml:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: minimal-aim-model-cache
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Gi
Create a service configuration in the service.yaml file:
apiVersion: v1
kind: Service
metadata:
name: minimal-aim-deployment
labels:
app: minimal-aim-deployment
spec:
type: ClusterIP
ports:
- name: http
port: 80
targetPort: 8000
selector:
app: minimal-aim-deployment
Customize the deployment further to fit your needs. Apply it to the Kubernetes cluster with kubectl:
kubectl apply -f . -n <YOUR_K8S_NAMESPACE>
4. Testing your deployment#
To test the deployment, execute an API call. First find the pod, then set up port forwarding.
kubectl port-forward service/minimal-aim-deployment 8000:80 -n <YOUR_K8S_NAMESPACE>
After port forwarding starts, the API is available locally at http://localhost:8000/.
4.1 Health check#
curl http://localhost:8000/healthz
The service answers with HTTP 200 when it is ready to accept requests. The startup probe and the liveness probe in section 3.1 use it.
/healthz answers a GET. An AIM can also give an endpoint that reports the state in more detail. That endpoint
takes a POST, and the section below names it if this AIM has one.
4.2 Endpoints#
This AIM serves one model endpoint and the shared AIM health and model-listing endpoints:
Method |
Path |
Purpose |
|---|---|---|
|
|
Design amino acid sequences for the backbone in one PDB file. |
|
|
Readiness. Forwards to the BentoML |
|
|
Lists the model the container serves. |
|
|
BentoML built-in health check. The service also keeps BentoML’s |
The service also answers GET /docs.json with the OpenAPI schema.
There is no /predict endpoint. Requests to /predict are not served; use /v1/inference.
The service answers no request, /health included, until the model is loaded and warmed up (see section 4.7, “Startup
and readiness”); before that the port is closed.
GET /health returns {"status": "ok"} with status 200 when the BentoML health check succeeds. It returns
{"status": "unavailable"} with the upstream status code if the check fails, and with 503 if the service cannot reach
its own health check. Use it as a readiness signal, not as a liveness signal.
GET /v1/models returns an OpenAI-style list with the single served model. metadata.accelerator is the name PyTorch
reports for the first visible GPU (null if it cannot be read):
{
"object": "list",
"data": [
{
"id": "dauparas/proteinmpnn",
"object": "model",
"metadata": {"accelerator": "<GPU name reported by PyTorch>"}
}
]
}
4.3 Request body#
The whole request is nested under a top-level request object:
Field |
Type |
Default |
Purpose |
|---|---|---|---|
|
string |
required |
Absolute path to the input PDB file inside the container. It must resolve to an existing file under |
|
integer |
|
Number of sequences to design, from 1 to 64. For larger sets, send repeated requests (see below). |
|
number |
|
Sampling temperature, greater than 0.0 and at most 1.0. Higher values give more diverse sequences. |
|
string |
|
ProteinMPNN model variant (the |
|
integer |
|
Random seed (the |
More than 64 sequences. One request returns at most 64 sequences. For a larger set, send repeated requests for the
same structure and concatenate the results. The seed must differ between the requests: ProteinMPNN seeds its random
generator from seed on every call, so the same structure, model, temperature and seed always return the same
sequences, whatever the temperature. Use a different seed for each request (or 0, which draws a random one from only
999 values, so duplicates across many requests are possible). Changing the temperature is not needed to get different
sequences, but it does change how diverse they are. Remove duplicate sequences after merging.
Limitation: all chains and all positions are designed. The service designs every chain of the input structure and
every residue position. It has no chain selection and no fixed positions: it cannot keep a chain, a binder interface or
a catalytic site unchanged while redesigning the rest. Upstream ProteinMPNN supports both (--pdb_path_chains,
--chain_id_jsonl, --fixed_positions_jsonl); they are not exposed by this AIM.
The service does not accept the PDB contents in the request. It reads the file from pdb_path, so mount the directory
that holds your structures into the container, and give the path as seen inside the container. AIM_INPUT_PATH
(default /workspace/data) is the root the service reads from, so mount the directory there, for example
-v /data/pdbs:/workspace/data:ro. Symbolic links are followed before the check, so a link that points outside the
root is rejected, as is any path with .. that leaves it.
Values outside the ranges above, an unknown model_name, a missing pdb_path, a pdb_path that is outside
AIM_INPUT_PATH or does not name an existing file, and malformed JSON are rejected with HTTP 400 before ProteinMPNN
runs. There is no batch form: send one request per structure.
4.4 Example request#
The example assumes a structure 1ubq.pdb in /data/pdbs on the host, mounted at /workspace/data in the container
(-v /data/pdbs:/workspace/data:ro):
Check readiness and the served model (the examples assume the service is reachable at localhost:8000; adjust the port
to the one you published):
curl http://localhost:8000/health
curl http://localhost:8000/v1/models
Run the design request:
curl http://localhost:8000/v1/inference \
-H "Content-Type: application/json" \
-d '{"request": {"pdb_path": "/workspace/data/1ubq.pdb", "num_seq_per_target": 8, "sampling_temp": 0.1, "seed": 37}}'
The same request in Python:
import requests
response = requests.post(
"http://localhost:8000/v1/inference",
json={
"request": {
"pdb_path": "/workspace/data/1ubq.pdb",
"num_seq_per_target": 8,
"sampling_temp": 0.1,
"seed": 37,
}
},
timeout=700,
)
response.raise_for_status()
result = response.json()
for sequence, score in zip(result["sequences"], result["scores"]):
print(score, sequence)
4.5 Response#
A successful response has this shape:
{
"sequences": ["MQIFVKTLTGK...", "MQIFVKTLTGK..."],
"scores": [1.23, 1.19],
"seq_recovery": [0.41, 0.39],
"native_score": 1.31
}
Key |
Purpose |
|---|---|
|
The designed sequences, in the order ProteinMPNN sampled them. The native (input) sequence that ProteinMPNN writes as the first FASTA record is not included. |
|
The |
|
The |
|
The |
The service runs ProteinMPNN’s own protein_mpnn_run.py code (main) with batch_size 1 and save_score 1, in the
service process. For the same request (including seed) the sequences are the ones the script gives on the command line.
4.6 Status codes#
Code |
Meaning |
Retry |
|---|---|---|
|
Success. |
— |
|
The request does not match section 4.3: a field is missing or out of range (including a |
No. Correct the request. |
|
The design run raised an error, for example because the file is not a valid PDB, or it produced no sequences. The response body carries a generic message; the detail is in the container log. |
Only after fixing the cause. |
|
The request did not finish within 600 seconds, counted from its arrival and including the time it waited for earlier requests ( |
Yes, later, with a smaller |
Errors are never returned as a 200 response with an error flag.
4.7 What to know before you deploy#
The service runs on one accelerator. The profiles bentoml-mi300x-fp32-tp1-throughput and
bentoml-mi355x-fp32-tp1-throughput use one accelerator each, in fp32 and without torch.compile. To serve more
requests per second, run more replicas.
The image sets AIM_METRIC=throughput, so that automatic profile selection finds the throughput profiles.
Startup and readiness. The service loads the model in its own process and does not start a process per request. At
startup it loads ProteinMPNN, then runs one warm-up design with each of the four weight files on a generated 64-residue
backbone (a scoring pass and sampling), and only then opens its port. /health, /healthz and /readyz therefore
answer only after the warm-up, and the first request is as fast as the later ones. The warm-up pays the one-time GPU
start-up cost (the first forward pass in a process). Use a readiness probe and wait for it before sending traffic. The warm-up
is logged to the container’s stderr (warm-up <model> done in ..., ProteinMPNN resident model ready on <device>).
Your input structures must be mounted; readiness does not check them. The service reads each structure from a file
inside the container, so mount a directory of PDB files under AIM_INPUT_PATH (default /workspace/data). The example of
section 4.4 uses 1ubq.pdb from that directory. The service does not look at the input directory when it starts, so
/readyz passes without it, and a request then fails with 400. With Docker, add this line to the command of section
1.1 or 1.2:
-v /path/to/pdbs:/workspace/data:ro \
With Kubernetes, create a persistent volume claim proteinmpnn-data that holds your PDB files, and add these entries to
the existing volumeMounts and volumes lists of the manifest in section 3.1:
volumeMounts:
- name: proteinmpnn-data
mountPath: /workspace/data
readOnly: true
volumes:
- name: proteinmpnn-data
persistentVolumeClaim:
claimName: proteinmpnn-data
Requests run one at a time, in arrival order. There is one model instance and one worker thread for all GPU work, so concurrent requests queue; they are not run in parallel on the GPU. Do not set more than one BentoML worker for this service: each worker would load its own copy of the model and warm up separately. To serve more traffic, run more replicas.
Memory. The four weight files are kept in memory, on the accelerator (about 7 MB each as stored; a small amount, tens of MiB, after the warm-up), and PyTorch’s caching allocator keeps the
memory it needed for the largest structure served so far. Long structures use more memory per request; the memory is
not given back between requests. A request that runs out of accelerator memory returns 500 and the cache is released.
Timeouts. A request waits for its result for at most 600 seconds in total, queueing time included. The model now runs inside the service process, so a run cannot be killed the way the earlier one-process-per-request design killed its process:
A request that has not started when the 600 seconds run out is cancelled and never runs (
503).A request that is already running when the 600 seconds run out gets
503at once, but its run goes on until it ends on its own and keeps the GPU until then. While such a run is active, every new request gets503(A previous request that timed out is still running on the GPU) immediately instead of queueing. When the run ends, the service accepts requests again.A run that never ends can be stopped only by restarting the container.
The service raises BentoML’s request timeout above this limit, so the 503 is the answer for a run that is too long;
BentoML’s default 60 second timeout, which would answer 504 earlier, does not apply. Set a client timeout above 600
seconds. Input size is the only lever on run time: it is not limited other than by this timeout and by accelerator
memory.
Output. The designed sequences and scores for the same request and seed are identical to those of the former one-process-per-request service.
The service needs a GPU and refuses to become ready without one. If PyTorch sees no accelerator, a weights file
cannot be provided (missing and the download fails, or the size or sha256 does not match), or the warm-up fails, the
service logs a critical error naming the cause (for example No GPU visible to PyTorch, or Could not provide ProteinMPNN weight <path> (source <URL>) or Model warm-up failed on device cuda: ...) and the BentoML worker exits. The container does not exit: BentoML’s supervisor restarts the worker about
every 10 seconds, so the message repeats, the container stays running and the service never becomes ready (/health
is unreachable). Use a readiness probe (for example /readyz) and watch the container logs to detect this; the exit
code and a restart policy will not. It does not fall back to the CPU unless you ask for it (PROTEINMPNN_DEVICE=cpu, see section 4.8).
The weights are not in the image; they live under AIM_CACHE_PATH. The four vanilla weight files
(v_48_002, v_48_010, v_48_020, v_48_030, about 27 MB in total, 6681301 bytes each) are stored in
$AIM_CACHE_PATH/proteinmpnn/vanilla_model_weights/ (default /workspace/model-cache/proteinmpnn/vanilla_model_weights/).
At startup the service logs this directory (to the container’s stderr, so it shows in docker logs) and checks each file (size and sha256). A missing file is downloaded on
first start from
https://raw.githubusercontent.com/dauparas/ProteinMPNN/905b0081f3aa3404d7ac22174bc25710f9995ff8/vanilla_model_weights/<name>.pt
(upstream commit of tag v1.0.1; the upstream repository is MIT-licensed), verified, and renamed into place
atomically; each download is logged. The first download takes tens of seconds (depending on the network). No Hugging Face token is needed. The first start therefore needs network access to
raw.githubusercontent.com, or pre-staged files. Mount a persistent volume at AIM_CACHE_PATH so the download
happens once.
Air-gapped use. Copy the four files from that upstream commit into
<cache volume>/proteinmpnn/vanilla_model_weights/ before starting; the service then makes no network request. A file
with the wrong size or sha256 stops startup.
Weights failure. If a download fails or a file does not verify, the container logs
ProteinMPNN startup failed: Could not provide ProteinMPNN weight <path> (source <URL>): <reason>. As described above,
the worker is restarted about every 10 seconds and the container keeps running without becoming ready.
4.8 Model environment variables#
Variable |
Default |
Effect |
|---|---|---|
|
|
Directory that holds |
|
|
Device of the resident model: |
|
|
Root of the model cache; weights are under |
|
|
Root directory that |
|
|
Second directory that |
PROTEINMPNN_DEVICE=cpu runs the model on the CPU and logs a warning. It is not the default and is only worth it for
very small inputs. With cpu, /v1/models reports
"accelerator": null.
The profiles also set TORCH_BLAS_PREFER_HIPBLASLT=1. Leave it as it is.
5. Advanced deployment scenarios#
5.1 Selecting a different profile#
Each profile of this AIM is pinned with AIM_PROFILE_ID. The profile identifier is the filename of the profile
without the .yaml extension. The README lists the available profiles and their accelerator counts.
docker run \
-e AIM_ID=dauparas/proteinmpnn \
-e AIM_PROFILE_ID=<PROFILE_ID> \
-v /path/to/model-cache:/workspace/model-cache \
--device=/dev/kfd --device=/dev/dri \
--group-add video --ipc=host \
-p 8000:8000 \
amdenterpriseai/aim-instinct-model-dauparas-proteinmpnn:2026.24.0-preview
Section 1.1 describes how the runtime uses AIM_PROFILE_ID. If the profile uses a different number of accelerators
than the default profile, change the accelerator count under resources in the section 3.1 manifest to match.
6. Monitoring and troubleshooting#
6.1 Getting help on the commands#
A general help command is available as follows:
docker run \
amdenterpriseai/aim-instinct-model-dauparas-proteinmpnn:2026.24.0-preview \
--help
A help command for specific subcommands is also available:
docker run \
amdenterpriseai/aim-instinct-model-dauparas-proteinmpnn:2026.24.0-preview \
<subcommand> --help
6.2 Enabling detailed logging#
docker run \
-e AIM_LOG_LEVEL=DEBUG \
-e AIM_ID=dauparas/proteinmpnn \
-e AIM_PROFILE_ID=bentoml-mi300x-fp32-tp1-throughput \
-v /path/to/model-cache:/workspace/model-cache \
--device=/dev/kfd --device=/dev/dri \
--group-add video --ipc=host \
-p 8000:8000 \
amdenterpriseai/aim-instinct-model-dauparas-proteinmpnn:2026.24.0-preview
6.3 Checking profile selection results#
It is possible to check which profile AIM selects from the given environment variables.
docker run \
-e AIM_ID=dauparas/proteinmpnn \
-e AIM_ACCELERATOR_COUNT=1 \
-e AIM_PRECISION=fp32 \
-e AIM_ACCELERATOR_MODEL=MI300X \
amdenterpriseai/aim-instinct-model-dauparas-proteinmpnn:2026.24.0-preview \
dry-run
6.4 List available profiles#
docker run \
amdenterpriseai/aim-instinct-model-dauparas-proteinmpnn:2026.24.0-preview \
list-profiles
6.5 Common problems#
The container restarts during the first start. The manifest in section 3.1 gives the start one hour. If the model needs more, raise
failureThresholdon the startup probe. Mount a persistent cache, so that later starts skip the download.The container restarts after it answers on
/healthz. The liveness probe accepts five minutes of failed checks. If the model does more work after the health endpoint answers, and that work takes longer, raisefailureThresholdon the liveness probe.The container stops with a profile selection error. The error message gives the cause.
If
AIM_PROFILE_IDis set, the ID matches no profile of this AIM. Compare it with the README or the output oflist-profiles(see section 6.4).If the error lists profile IDs, the matching profiles must be selected explicitly. Set
AIM_PROFILE_IDto one of them.If no profile is compatible, the error counts the profiles per reason, such as
accelerator_mismatchorprecision_mismatch. Compare theDetected AcceleratorandAccelerator Countlines in the log, and anyAIM_PRECISIONorAIM_METRICyou set, with the profile table in the README. Or setAIM_PROFILE_IDto a profile from the README (see section 1.1).
/healthzdoes not answer yet. The service binds the port only after the model is loaded. Read the container logs to see how far startup got.
7. Security considerations#
Use Kubernetes secrets or environment variables for sensitive credentials
Implement appropriate network policies to restrict access to your deployment
The model cache holds downloaded model weights. Restrict write access to it