Custom Models#
A custom model lets you deploy a model whose architecture isn’t published in the AMD AIM catalog. You combine a generic AIM base image — which supplies the runtime, engine, and tested accelerator profiles — with your own model weights pulled from S3, HuggingFace, or any other supported source.
v1alpha2
Custom models use the aim.eai.amd.com/v1alpha2 API. The v1alpha1 spec.custom / spec.modelSources flow on AIMModel is deprecated and removed from v1alpha2.
When to use this flow#
Goal |
Use this flow? |
|---|---|
Deploy a model whose architecture is in the AMD AIM catalog (with your own fine-tune weights) |
No — use Fine-Tuned Models |
Deploy a model whose architecture is not in the AMD AIM catalog |
Yes |
Deploy a published AIM image without modification |
No — apply the AIMModel and you’re done |
Mental model#
The flow has two AIMModels working together:
Base-image model — points
spec.imageat an AIM base image and produces base AIMProfiles: one per accelerator/precision combination the base image ships, with noaimIdormodelSourcespopulated. Base profiles are not deployable on their own — they exist only as derivation sources.Custom-model AIMModel — uses
spec.profiles.derivedFromto select those base profiles and overlay your model identity and weights. The result is a set of regular deployable AIMProfiles that anAIMServicecan resolve through.
The base-image model is typically applied once by the platform team and reused across many custom-model derivations.
Prerequisites#
To use a container as a base image for custom models, it must:
Ship profile YAMLs under
/workspace/aim-runtime/profiles/general/. The v1alpha2 discovery walker only scansgeneral/when the image’sAIM_IDis empty (which is the case for a generic base image).Populate
aim_idin each profile YAML — or leave it empty. If empty, the derivation supplies the architecture identity viaoverrides.aimId(see Identity stamping below). Both are supported.Leave
model_idandmodel_sourcesempty in those YAMLs. That’s what makes a profile a “base profile” rather than a published deployable.
Base profile YAMLs do carry precision, metric, acceleratorModel, acceleratorCount, and engineArgs — these are intrinsic tuning artefacts of the base image and the whole reason a base profile is useful as a derivation source. Derivation copies them through to each derived profile (unless an overrides.* field replaces them), so a base image that ships one (MI300X, fp8, latency) profile and one (MI325X, fp16, throughput) profile produces two derived deployable profiles per model.
AMD publishes amdenterpriseai/aim-base as a ready-to-use base image. You can also build your own following the layout described above.
Step 1: Apply the base-image AIMModel#
apiVersion: aim.eai.amd.com/v1alpha2
kind: AIMModel
metadata:
name: aim-base-vllm
namespace: ml-team
spec:
image: amdenterpriseai/aim-base:0.11
imagePullSecrets:
- name: registry-creds
serviceAccountName: aim-runtime
Discovery runs as a Kubernetes Job that inspects the image and writes its profile catalog into a ConfigMap (referenced by status.discoveryCacheRef). For each supported profile in the catalog, the model controller creates a base AIMProfile.
Wait for discovery to complete and base profiles to materialise:
kubectl -n ml-team get aimmodel aim-base-vllm \
-o jsonpath='{.status.managedProfiles}'
# {"deployable":0,"notAvailable":0,"ready":8,"base":8,"total":8}
base > 0 and deployable == 0 is the canonical “this is a base-image model” signal.
The base profiles themselves carry distinct labels and status:
kubectl -n ml-team get aimprofile \
-l aim.eai.amd.com/source-model=aim-base-vllm \
-o custom-columns=NAME:.metadata.name,ROLE:.metadata.labels.aim\\.eai\\.amd\\.com/profile-role,DEPLOYABLE:.status.deployable
# NAME ROLE DEPLOYABLE
# aim-base-vllm-general-fp8-lat-... base false
# aim-base-vllm-general-fp16-lat-... base false
# ...
Step 2: Apply the custom-model AIMModel#
apiVersion: aim.eai.amd.com/v1alpha2
kind: AIMModel
metadata:
name: acme-custom-transformer
namespace: ml-team
spec:
profiles:
derivedFrom:
selector:
role: base
modelRef:
name: aim-base-vllm
scope: Namespace
versionPolicy: all
overrides:
aimId: acme/custom-transformer
modelId: acme/custom-transformer
modelSources:
- modelId: acme/custom-transformer
sourceUri: s3://acme-models/custom-transformer
imagePullSecrets:
- name: registry-creds
serviceAccountName: aim-runtime
The spec splits cleanly into “what to filter” (selector) and “what to stamp on the derived profile” (overrides):
Field |
Half |
Purpose |
|---|---|---|
|
filter |
Match only base profiles. Required — without this, only already-deployable profiles are considered and no base profiles match. |
|
filter |
Pin the derivation to one specific base-image AIMModel. Without this, every base-image model in scope contributes candidates. |
|
stamp |
Target architecture identifier for the derived profile. Required by CEL when |
|
stamp |
Target model identifier. Required by CEL when |
|
stamp |
The BYO weights that complete the base into a deployable profile. |
CEL validation on AIMModelProfilesSpec enforces the split: setting selector.aimId, selector.modelId, or selector.profileId when selector.role=base is rejected at admission time. Identity fields on the selector were a filter on the source (matching the source’s identity); base profiles by design have no source identity, so the field is meaningless there.
When overrides.modelId is unset and overrides.modelSources[0].modelId is, the derived profile’s spec.modelId auto-derives from the latter. The explicit override always wins, and is required for role=base regardless of modelSources.
Wait for the derivation to produce deployable profiles:
kubectl -n ml-team get aimmodel acme-custom-transformer \
-o jsonpath='{.status.managedProfiles}'
# {"deployable":8,"notAvailable":0,"ready":8,"base":0,"total":8}
kubectl get aimmodel shows the resolved flow under the KIND column — a correctly-configured custom model surfaces as Custom. If you see Image there, your spec is using spec.image (the official flow) rather than spec.profiles.derivedFrom; if you see Derived, the selector is matching deployable rather than base profiles. See status.kind for the full discriminator table.
Step 3: Deploy with an AIMService#
apiVersion: aim.eai.amd.com/v1alpha2
kind: AIMService
metadata:
name: acme-custom-transformer-service
namespace: ml-team
spec:
model:
name: acme-custom-transformer
The AIMService resolver treats spec.model.name as a shortcut for “every deployable profile produced by this model” and ranks the surviving candidates by primary > type > version. See Deploying Services for the other resolution shapes (by-profile-name, by-selector, with profile overlay).
Identity stamping#
Target identity for the derived profile lives entirely in overrides. The selector is filter-only.
Field |
Behaviour |
Required when |
|---|---|---|
|
Stamped on derived profile. Wins over the source’s |
Yes (CEL) |
|
Stamped on derived profile. Wins over the source’s |
Yes (CEL) |
|
Stamped on derived profile. Wins over the source’s |
No |
For role=deployable remixes (where you’re re-derivating an already-deployable profile to e.g. expand across accelerator variants), all three identity overrides are optional — the source profile’s existing identity carries through if you don’t override it. For role=base derivations they’re required (well, aimId and modelId) because the source has no identity to inherit.
This is enforced at admission time by CEL on both AIMProfileSetSpec and AIMModelProfilesSpec:
selector.aimId/modelId/profileId are not allowed when selector.role=base;
set overrides.aimId/modelId/profileId instead (base profiles have no
source identity to filter on)
selector.role=base requires overrides.aimId and overrides.modelId
(base profiles carry no identity of their own; overrides supply it)
So an attempt to set selector.aimId under selector.role=base is rejected by the API server before reaching the controller.
Common patterns#
Filter base profiles by accelerator#
A base image typically ships profiles for several GPUs. Narrow the derivation to just the ones you want:
spec:
profiles:
derivedFrom:
selector:
role: base
modelRef:
name: aim-base-vllm
acceleratorModel: MI300X
acceleratorCount: 2
overrides:
aimId: acme/custom-transformer
modelId: acme/custom-transformer
modelSources:
- modelId: acme/custom-transformer
sourceUri: s3://acme-models/custom-transformer
Pin to a specific precision#
spec:
profiles:
derivedFrom:
selector:
role: base
modelRef:
name: aim-base-vllm
precision: fp8
metric: latency
overrides:
aimId: acme/custom-transformer
modelId: acme/custom-transformer
modelSources:
- modelId: acme/custom-transformer
sourceUri: s3://acme-models/custom-transformer
Authenticate to private weights#
overrides.modelSources[].env accepts the same credential references as a normal profile:
spec:
profiles:
derivedFrom:
selector:
role: base
aimId: acme/custom-transformer
modelRef:
name: aim-base-vllm
overrides:
modelSources:
- modelId: acme/custom-transformer
sourceUri: s3://acme-models/custom-transformer
env:
- name: AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: s3-credentials
key: access-key
- name: AWS_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: s3-credentials
key: secret-key
Cluster-scoped variant#
For platform-managed custom models that should be visible across namespaces, use AIMClusterModel with the same spec.profiles shape. The derivation will produce AIMClusterProfile objects instead of namespace-scoped ones, and modelRef.scope can be set to Cluster to pin to a cluster-scoped base-image model.
Troubleshooting#
Base-image model status shows base: 0#
Discovery hasn’t found any profiles to materialise. Common causes:
The base image’s profile YAMLs aren’t under
/workspace/aim-runtime/profiles/general/.The image has
AIM_IDset, so discovery is looking under$AIM_ID/instead ofgeneral/.The profile YAMLs populate
model_idormodel_sources— they’re treated as deployable, not base.
Inspect the discovery cache:
kubectl -n ml-team get aimmodel aim-base-vllm \
-o jsonpath='{.status.discoveryCacheRef.name}'
# aim-base-vllm-discovery-<hash>
kubectl -n ml-team get configmap aim-base-vllm-discovery-<hash> -o yaml
Custom-model AIMModel is NotAvailable with NoMatchingProfiles#
The selector matched zero base profiles. Verify:
# Check that base profiles exist under the modelRef
kubectl -n ml-team get aimprofile \
-l aim.eai.amd.com/source-model=aim-base-vllm,aim.eai.amd.com/profile-role=base
# Check the derivation's profile set
kubectl -n ml-team get aimmodel acme-custom-transformer \
-o jsonpath='{.status.profileSetRef.name}'
kubectl -n ml-team get aimprofileset <profileset-name> -o yaml
Common causes:
selector.modelRef.namedoesn’t match the base-image model’s metadata name.selector.modelRef.scopemismatches the base-image model’s scope (useAutoif unsure).An accelerator/precision filter excluded every candidate.
AIMService stuck in Pending with ProfileNotFound#
Confirm the derivation produced deployable profiles before debugging the service:
kubectl -n ml-team get aimmodel acme-custom-transformer \
-o jsonpath='{.status.managedProfiles}'
If deployable == 0, fix the derivation first. If deployable > 0, the service-side resolver isn’t finding them — check that the service’s spec.model.name matches the custom-model AIMModel’s name exactly, and that any spec.profile.selector you set doesn’t narrow more than the available profiles.
Next Steps#
Fine-Tuned Models — Derive from existing official AIM profiles instead of base-image base profiles
Deploying Services — Resolution shapes and overlay patterns for
AIMServiceAIM Models — Full lifecycle of all three model flows
AIM Profile Sets — The derivation machinery underneath
spec.profilesProfiles — Base vs deployable, provenance labels, status fields