AIM Services#
AIMService is the resource you create to deploy an inference endpoint. It binds an AIMProfile (the runtime configuration) to optional service-level concerns — caching, routing, autoscaling, resources — and produces a KServe InferenceService plus an optional Gateway API HTTPRoute.
v1alpha2
This page documents the aim.eai.amd.com/v1alpha2 API. For the v1alpha1 spec.template shape see Legacy AIMService.
Lifecycle#
On every reconcile the controller:
Resolves a profile — by name, by selector, or by treating
spec.model.nameas a shortcut for “every deployable profile produced by that model”.Materialises an overlay AIMProfile if
spec.profileOverridesis set.Pre-warms the cache by creating an
AIMProfileCachefor the resolved profile (when caching is enabled).Builds an InferenceService mounting the cache PVC, the profile’s container image, env, and resource requirements.
Creates an HTTPRoute if routing is enabled and a
gatewayRefis configured.Reports component health in
status.conditionsand the high-levelstatus.statusrollup.
Resolution shapes#
A v1alpha2 AIMService reaches a deployable AIMProfile through one of five shapes. They share the same spec.model / spec.profile fields but combine them differently.
Shape |
Spec |
Use case |
|---|---|---|
|
You know exactly which profile to deploy |
|
|
You know the model; let the controller pick the best deployable profile |
|
|
Narrow a model’s profile pool by precision / accelerator / engine |
|
|
Reach profiles via labels — |
|
|
One-shot deploy straight from a container image; the controller looks up or auto-creates an |
In all shapes the controller ranks the surviving candidates by primary > type > version and resolves to a single profile. The resolved profile is recorded in status.resolvedProfile (name, scope, kind, uid).
By name#
The lowest-friction shape — references a specific AIMProfile or AIMClusterProfile.
apiVersion: aim.eai.amd.com/v1alpha2
kind: AIMService
metadata:
name: qwen-chat
namespace: ml-team
spec:
profile:
name: qwen-qwen3-32b-mi300x-fp8-latency
Resolution order is namespace first (AIMProfile), then cluster (AIMClusterProfile). Namespace wins.
By model#
You name the model, the controller picks the profile. This is the recommended default.
spec:
model:
name: qwen-qwen3-32b
spec.model.name is shorthand for spec.profile.selector.modelRef.name plus the implicit role=Deployable filter. The candidate pool is every deployable profile owned by that AIMModel (or AIMClusterModel of the same name). Ranking selects the best.
Model + selector#
Narrow a model’s profile pool by hardware or engine criteria.
spec:
model:
name: qwen-qwen3-32b
profile:
selector:
precision: fp8
acceleratorModel: MI300X
metric: latency
The selector adds AND filters on top of the model-derived modelRef.name. Any selector field documented under Profile Selectors is supported except role (see below).
Global selector#
Reach profiles via labels alone, without anchoring to a model.
spec:
profile:
selector:
aimId: qwen/qwen3-32b
precision: fp8
acceleratorModel: MI300X
The CRD requires at least one of selector.aimId or selector.modelRef.name so the controller’s watch fan-out can find this service via O(1) label indexes.
By image#
For one-shot deploys when you only have an AIM container image. Requires the migration-window annotation to opt into the v1alpha2 profile pipeline (the legacy v1alpha1 template pipeline is the default for image-only services until v1alpha1 is removed — see Migration window).
apiVersion: aim.eai.amd.com/v1alpha2
kind: AIMService
metadata:
name: qwen-chat
namespace: ml-team
annotations:
aim.eai.amd.com/reconciler-pipeline: profile
spec:
model:
image: amdenterpriseai/aim-qwen-qwen3-32b:0.8.5
The resolver looks up an existing AIMModel/AIMClusterModel whose spec.image matches. If exactly one matches, the resolver treats it as if you had written spec.model.name: <that model> and ranks as usual. If none matches, the plan step creates a dedicated, service-owned AIMModel in the service’s namespace with:
metadata.ownerReferencespointing at the service (garbage collected with the service).Label
aim.eai.amd.com/origin=auto-generated.Annotation
aim.eai.amd.com/auto-source=spec.model.image.
If multiple existing models match the same image (rare; not recommended), the resolver reports ProfileNotFound and asks you to disambiguate via spec.model.name.
Image-shape services pair best with caching.mode: Shared only when you intentionally apply a long-lived AIMModel for the image and reference it via spec.model.name instead — auto-created models are per-service.
Reserved selector fields#
spec.profile.selector.role is reserved on AIMService. The controller forces role: deployable at evaluation time. Omitting the field is fine; explicitly setting role: deployable is fine; setting role: base (or any other value) is rejected at admission.
This guarantees a service never resolves to a base profile.
Profile binding stability#
The resolver is sticky-once-bound: once status.resolvedProfile is set, the controller keeps using that profile on every subsequent reconcile, provided the profile still exists and still satisfies the current selector.
This protects running services from silently switching profiles when the candidate pool changes for reasons unrelated to the service spec. The classic trigger is adding a second AIMModel in the same namespace with overlapping aimId — without stickiness, the ranker might pick a different-precision profile from the newer model and rebind the running service to fp8 weights when it was happily serving fp16, complete with a fresh cache download and a predictor pod roll.
When the binding is re-evaluated automatically#
A natural rebind happens when the existing binding is no longer usable:
Trigger |
Mechanism |
|---|---|
Bound profile deleted |
The |
Bound profile no longer matches the current selector |
The user narrowed |
Bound profile drops below the |
The bound profile’s |
Transient infrastructure error fetching the bound profile |
Re-rank rather than risk serving a stale binding through an outage. |
Each natural rebind emits a Normal event with reason ProfileRebound naming the previous profile, the new profile, and the trigger reason — so kubectl describe aimservice always tells you why a binding changed.
Forcing a rebind explicitly#
When you intentionally want the resolver to pick again (e.g. an image upgrade landed a new published profile and you want the service to adopt it), set the aim.eai.amd.com/force-rebind annotation to any non-empty value:
kubectl annotate aimservice my-svc \
aim.eai.amd.com/force-rebind=now --overwrite
On the next reconcile the resolver ignores the sticky binding, re-ranks from scratch, and (if the winner differs) emits the same ProfileRebound event with force-rebind as the reason.
The operator does NOT clear the annotation. Remove it once the desired rebind has happened to return to sticky behavior:
kubectl annotate aimservice my-svc aim.eai.amd.com/force-rebind-
Leaving the annotation in place is safe — the ranker is deterministic and ProfileRebound only fires when the winner actually changes — but it forgoes the stability guarantee of the sticky default, so the next time a higher-ranked candidate appears the service will silently adopt it.
Pinning to a specific profile#
Stickiness is a default-on protection, not a guarantee. The strongest guarantee is spec.profile.name: by name there’s no ranking at all, so the binding is fixed for the lifetime of the spec.
Profile overlays#
Sometimes you want to tweak a published profile for one service without forking it. spec.profileOverrides creates a service-owned overlay AIMProfile derived from spec.profile.name:
spec:
profile:
name: qwen-qwen3-32b-fp8-mi300x
profileOverrides:
modelSources:
- modelId: my-org/qwen3-32b-finetune
sourceUri: hf://my-org/qwen3-32b-finetune
containerEnv:
- name: AIM_OVERRIDE_NOTE
value: "service overlay rebases the seed profile onto custom weights"
What happens:
The controller materialises an
AIMProfilenamed<seed>-<service>-overlay-<hash>in the service’s namespace.The overlay carries
profile-role: deployable,profile-origin: derived, and an annotation pointing back at the service.Downstream (cache, ConfigMap, KServe wiring) keys off the overlay — so the override participates in cache-key computation, not just runtime env wiring.
The overlay is owner-referenced by the service and garbage-collected on service deletion.
The override field set mirrors ProfileOverrides used by AIMProfileSet derivation — the same internal primitive applies both.
spec.profileOverrides requires spec.profile.name; it can’t be combined with a selector-based resolution shape.
Caching#
When the resolved profile has spec.caching.enabled: true (namespace-scoped profiles only) or the service has its own spec.caching block, the controller creates an AIMProfileCache that pre-downloads the profile’s modelSources onto a PVC before the InferenceService starts.
Mode |
Behavior |
|---|---|
|
The cache PVC is shared across services using the same profile. Reused indefinitely; outlives any single service. |
|
Each service gets its own cache PVC, garbage-collected with the service. |
spec:
caching:
mode: Dedicated
See Model Caching for the full hierarchy (AIMProfileCache → AIMArtifact → PVC + Download Job).
Resources and replicas#
Field |
Description |
|---|---|
|
Fixed replica count. Default |
|
Enables KEDA-driven autoscaling between the bounds. |
|
Custom metric configuration (typically vLLM OTEL metrics). See Scaling and Autoscaling. |
|
Optional override for container resources. Merged on top of the resolved profile’s |
|
Service-level secrets, merged with the profile’s. |
|
Pod service account for the inference workload. |
|
Pod priority class. |
Routing#
Optionally expose the service through Gateway API:
spec:
routing:
enabled: true
gatewayRef:
name: inference-gateway
namespace: gateways
pathTemplate: "/team/{.metadata.labels['team']}/chat"
The controller creates an HTTPRoute that attaches to the referenced Gateway. The pathTemplate supports JSONPath expressions referencing service metadata. See Routing and Ingress.
Status#
High-level status#
Status |
Meaning |
|---|---|
|
Resolving the profile or waiting on upstream dependencies |
|
Profile resolved; creating cache and InferenceService |
|
InferenceService is ready and serving traffic |
|
Partially functional — typically routing or cache issues with a healthy inference path |
|
Terminal error preventing deployment |
Status fields#
Field |
Description |
|---|---|
|
The profile that was resolved ( |
|
The runtime config (if any) that contributed defaults |
|
Cache state — |
|
Replica counts when autoscaling is active ( |
|
Observed routing including the rendered HTTP path |
|
Per-component conditions — see below |
Conditions#
The v1alpha2 service reports these component conditions:
Condition |
What it tracks |
|---|---|
|
Profile resolution ( |
|
|
|
KServe |
|
Gateway API route acceptance (only when routing is enabled) |
|
KEDA-managed autoscaler health (only when autoscaling is enabled) |
|
Aggregate of all above |
When ProfileReady=False (profile not found, or resolved to a base profile), the controller suppresses the downstream InferenceServiceReady / HTTPRouteReady entries — those resources aren’t being created until the profile resolves, so reporting them as “Creating” would be misleading.
See Conditions Reference for the full reason catalogue.
Validation#
CEL admission rules reject a handful of v1alpha1-isms outright:
Rejected on v1alpha2 |
Use instead |
|---|---|
|
|
|
|
Neither |
Set at least one |
|
Provide |
|
Omit |
|
Add one of those anchor fields |
See Resource lifecycle → validation for the canonical list of validation rules across all v1alpha2 CRDs.
Examples#
Minimal deploy by name#
apiVersion: aim.eai.amd.com/v1alpha2
kind: AIMService
metadata:
name: qwen-chat
namespace: ml-team
spec:
profile:
name: qwen-qwen3-32b-mi300x-fp8-latency
Auto-select for a model, with autoscaling#
apiVersion: aim.eai.amd.com/v1alpha2
kind: AIMService
metadata:
name: qwen-chat
namespace: ml-team
spec:
model:
name: qwen-qwen3-32b
minReplicas: 1
maxReplicas: 5
autoScaling:
metrics:
- type: PodMetric
podmetric:
metric:
backend: opentelemetry
metricNames:
- vllm:num_requests_running
query: "vllm:num_requests_running"
operationOverTime: avg
target:
type: Value
value: "1"
Autoscaling between minReplicas and maxReplicas requires at least one metric (or minReplicas: 0 for scale-from-zero); otherwise the spec is rejected with ConfigValid=False (reason AutoscalingRequiresMetrics).
Custom weights via overlay#
apiVersion: aim.eai.amd.com/v1alpha2
kind: AIMService
metadata:
name: qwen-finetune
namespace: ml-team
spec:
profile:
name: qwen-qwen3-32b-mi300x-fp8-latency
profileOverrides:
modelSources:
- modelId: acme/qwen3-32b-finetune
sourceUri: hf://acme/qwen3-32b-finetune
Selector with HTTP routing#
apiVersion: aim.eai.amd.com/v1alpha2
kind: AIMService
metadata:
name: qwen-chat
namespace: ml-team
spec:
profile:
selector:
aimId: qwen/qwen3-32b
precision: fp8
acceleratorModel: MI300X
metric: latency
routing:
enabled: true
gatewayRef:
name: inference-gateway
namespace: gateways
Troubleshooting#
Service stuck in Pending with ProfileNotFound#
The resolver found no candidates. Check:
kubectl get aimservice <name> -o jsonpath='{.status.conditions[?(@.type=="ProfileReady")].message}'
Common causes:
spec.profile.nametypo or wrong scope (namespace vs cluster).spec.model.namedoesn’t match any AIMModel/AIMClusterModel that has produced deployable profiles. Checkkubectl get aimmodel <name> -o jsonpath='{.status.managedProfiles}'.Selector is too restrictive — relax
precision,acceleratorModel, oracceleratorCount.
Service ProfileReady=False / BaseProfile#
spec.profile.name points at a base profile (status.deployable=false). Base profiles can’t back services directly — either reference a deployable profile, or derive a deployable one through a custom-model AIMModel (Custom Models) and reference that.
Service is Running but throughput is unexpected#
The resolved profile may not be the one you expected. Check status.resolvedProfile:
kubectl get aimservice <name> -o jsonpath='{.status.resolvedProfile}'
If the wrong profile was selected, narrow spec.profile.selector (or switch to spec.profile.name) to pin the choice.