LoRA Adapters#
AIM Engine can serve LoRA adapters alongside a base model. A service declares
adapters in spec.adapters; spec.adapterMode selects the contract (static,
the default, or dynamic). The declared adapters are staged into an isolated,
per-service subtree of a shared adapter disk and mounted read-only into the
inference container at /adapters.
spec.adapterMode is an immutable, startup-time property of the pod (it maps
directly to the AIM_ADAPTER_MODE container env):
|
Adapter disk mounted? |
|
|---|---|---|
|
When ≥ 1 adapter is declared |
No — frozen at creation |
|
Yes (even at zero adapters) |
Yes — add/remove anytime |
Because adapter add/remove is a pure disk operation, the mount is decoupled from
the list length: in dynamic mode the subtree is mounted even at zero
adapters, so editing the list — including dropping to zero — never restarts the
pod or modifies the InferenceService, and the runtime loads/unloads adapters from
the mounted subtree at will. In static mode the set is fixed at creation and the
disk is mounted only when at least one adapter is declared (a static service with
no adapters serves nothing and needs no mount).
Both pipelines
spec.adapters is supported on both the profile pipeline
(aim.eai.amd.com/v1alpha2, via spec.profile) and the template
pipeline (aim.eai.amd.com/v1alpha1, via spec.template / spec.model).
The staging mechanics are identical; only base-model resolution differs —
the profile pipeline resolves the parent through the AIMProfileCache, the
template pipeline through the AIMTemplateCache. On v1alpha2, adapters
still require spec.profile.
Minimal MVP scope
This release ships reference-only adapter support: adapters are plain
references to existing AIMArtifact objects, and status reflects disk-side
staging only (Pending / Downloading / Downloaded). The list is editable
— adding an adapter stages it and the runtime hot-loads it. Removing an entry
is reconciled: a controller-managed subtree-sync Job prunes the removed
adapter’s directory from the service subtree (the model-artifact reaper still
reclaims the whole subtree when the service is deleted). Note the in-pod
effect of a removal depends on the image running in dynamic mode (watcher);
in static mode the bytes are removed from disk but the running pod keeps the
adapter until restart. The controller already sets the AIM_ADAPTER_*
container env (AIM_ADAPTER_SOURCE, AIM_ADAPTER_MODE, the MAX_* caps, and
a dynamic-mode refresh interval), but the image honouring them (the in-pod
watcher), inline self-healing (sourceUri on the service), engine-reported
Loaded / LoadRejected states, dynamic namespace opt-in, and
AIMProfileCache → adapterDisk auto-propagation are deferred.
Concepts#
Object |
Role |
|---|---|
|
The base model. Provisions two PVCs: the model cache PVC and a shared |
|
A LoRA adapter bound to a parent model via |
|
The list of adapters this service serves. Editable after creation; entries are pure references with unique |
Storage layout#
The shared adapter disk is one RWX PVC owned by the base model artifact. Each service that serves adapters gets its own subtree keyed by the service UID:
<adapter-disk>/
<service-uid>/ # mounted read-only into the pod via subPath
<adapter-path>/ # one directory per adapter
.staging/ # download scratch (PVC root, outside the pod mount)
.aside/ # pre-promote swap area
A per-(service, adapter) staging Job downloads the adapter into .staging/,
verifies it, and atomically promotes it into the live subtree with rename(2).
The staging Job is the single logical writer to the live tree — the controller
never mounts the PVC. The pod mounts only <service-uid>/ (read-only, via
subPath), so a service can only ever see the adapters it declares.
Because the controller never mounts the PVC, a fast, AIMService-owned
subtree-sync Job reconciles the per-service directory: it creates
<service-uid>/ (so the read-only subPath mount binds — the aim-runtime
errors on startup if its mounted subPath is missing) and prunes any adapter
directory no longer in spec.adapters. The InferenceService is gated only on
the subtree existing — not on downloads. The Job’s name encodes the declared
set and status.adapterSubtreeSyncKey records the last synced set, so editing
spec.adapters re-runs the sync (to prune removed adapters) without re-run
loops. Staging Jobs run asynchronously and the runtime hot-loads each adapter
as it lands. The controller sets AIM_ADAPTER_SOURCE to the mount path so the
image finds the subtree.
MVP bootstrap contract#
Because AIMProfileCache → adapterDisk propagation is deferred, the operator
must satisfy the following before a service can serve adapters:
Pre-create the parent model artifact with
adapterDisk. A service that declares adapters against a parent without an adapter disk is gated withParentLacksAdapterDisk.Use
Sharedcaching and an exactsourceUrimatch. The service resolves its parent base model by matching the resolved base model id against the cache’s resolved artifacts — theAIMProfileCacheon the profile pipeline, theAIMTemplateCacheon the template pipeline.Dedicatedcaching mints a per-service parent and is not supported with adapters.First model source wins. Adapters bind to a single base model. If the resolved profile or template carries more than one model source, the first is targeted deterministically.
Each adapter’s
parentArtifactmust equal the resolved base model. A mismatch is reported as a configuration error.
Worked example#
# 1. Parent base model with an adapter disk (operator-created bootstrap).
apiVersion: aim.eai.amd.com/v1alpha1
kind: AIMArtifact
metadata:
name: gemma-3-27b-it-cache
spec:
type: model
modelId: google/gemma-3-27b-it
sourceUri: hf://google/gemma-3-27b-it
size: 60Gi
adapterDisk:
size: 50Gi
---
# 2. A shared, pre-authored adapter bound to the parent.
apiVersion: aim.eai.amd.com/v1alpha1
kind: AIMArtifact
metadata:
name: cs-tone-v3
spec:
type: adapter
modelId: acme/cs-tone-v3
parentArtifact: gemma-3-27b-it-cache
sourceUri: s3://eai-artifacts/adapters/cs-tone-v3/
rank: 16
---
# 3. A service that serves the adapter.
apiVersion: aim.eai.amd.com/v1alpha2
kind: AIMService
metadata:
name: gemma-cs
spec:
profile:
name: gemma-3-27b-it-mi300x-bf16
adapterMode: dynamic
adapters:
- name: cs-tone-v3
kind: AIMArtifact
The full set of objects is also available under
config/samples/aim_v1alpha2_lora_adapters.yaml.
The same adapters can be served from the template pipeline by declaring
spec.adapters on a v1alpha1 service (the parent base model is then resolved
through the AIMTemplateCache):
apiVersion: aim.eai.amd.com/v1alpha1
kind: AIMService
metadata:
name: gemma-cs
spec:
model:
name: gemma-3-27b-it
adapterMode: dynamic
adapters:
- name: cs-tone-v3
kind: AIMArtifact
Lifecycle and status#
The InferenceService is gated until the base model is Ready and the
per-service adapter subtree exists (the subtree-sync Job has succeeded).
Adapters then stage asynchronously and the runtime loads them as they land —
downloads never hold back serving. The service exposes a single aggregate
Adapters condition (rather than one condition per adapter) and per-adapter
disk-side states under status.adapters[]. Once the subtree exists the
Adapters condition is Ready even while individual adapters are still
downloading; a configuration error is the only adapter state that blocks the
InferenceService:
status:
adapters:
- name: cs-tone-v3
adapterPath: cs-tone-v3
modelId: acme/cs-tone-v3
state: Downloaded
Reason |
Meaning |
|---|---|
|
The resolved base model has no adapter disk yet ( |
|
The per-service subtree is being created; the ISVC is gated on this ( |
|
Multiple model sources, a parent mismatch, a duplicate adapter path, or a non-adapter artifact was referenced ( |
|
The subtree is ready and serving; one or more adapters are still downloading asynchronously ( |
|
All declared adapters are staged ( |
Reclaim#
Adapter subtrees are not Kubernetes objects, so owner-reference garbage collection cannot reclaim them. Cleanup is two-tier, split by ownership:
Per-adapter unload (service alive) — the AIMService-owned subtree-sync Job prunes adapter directories no longer in
spec.adapters. It is owned by the service (garbage-collected with it) and runs whenever the declared set changes.Whole-subtree reclaim (service deleted) — the base model artifact periodically launches a reaper Job that removes subtrees whose owning
AIMServiceno longer exists, plus crash-orphaned.staging/.asidedirectories. This is owned by the model artifact (not the service) precisely so it can run after the service — and its subtree-sync Job — are gone. Deleting an adapter-serving service tears down its pods immediately; its on-disk subtree is reclaimed by the next sweep.
Serving contract#
The container is expected to load every adapter directory it finds under
/adapters. The controller sets the AIM_ADAPTER_* environment variables on the
inference container (AIM_ADAPTER_SOURCE, AIM_ADAPTER_MODE, the MAX_* caps,
and a dynamic-mode refresh interval); the MAX_* caps are currently uniform
built-in defaults. The image honouring these variables, a profile
LoRA-support gate, max-rank validation, and engine-reported load state remain to
be completed alongside the inference container.