The module lifecycle stage: Experimental
The module has requirements for installation
This page walks one order end to end and says why each part is shaped the way it is. Everything it
describes holds from v0.0.1 of the module onward.
It is not the place to learn today’s behaviour in short form. For that:
- User guide — order inference, read the status, find out why an order waits;
- Administration guide — enable the module, author classes, grant access, watch, disable;
- Examples — the manifests, without the narrative.
The ai-inference controller deploys the order workload itself: a StatefulSet with the request runtime, a Service, exposure, a device claim and the rest of the order objects. No delivery package is involved.
When the module is enabled, Helm creates cluster-scoped default-llm (External, Token) with the standard module: ai-inference label, replica bounds and an accelerator policy that permits a shared whole device. Use it directly: the order asks for no accelerator, and the platform assigns the device, the count and the share of it.
1. InferenceServiceClass
Bundled class (module)
After module install the cluster already has:
apiVersion: ai.deckhouse.io/v1alpha1
kind: InferenceServiceClass
metadata:
name: default-llm
spec:
modelPolicy:
allowedEndpointTypes:
- Chat
- Embeddings
- Rerank
exposurePolicy:
type: External
authentication: Token
https:
mode: CertManager
certManager:
clusterIssuerName: selfsignedExpected: status.phase: Ready.
allowedEndpointTypes is what lets an order name only a class and a model: an order names no
contract of its own, and the platform picks one from this list. The bundled class permits every
contract the module supports — Chat, Embeddings and Rerank — and the ORDER of the list
matters, because the platform takes the first element of the effective set.
The effective set is this list narrowed by whatever the source of the model says the model can
serve. On the ai-models catalog path that narrowing happens: an embedding model is served as an
embedding model on this very class. On the direct Hugging Face path there are no such facts, so
the whole list stands and the FIRST contract wins — Chat. An order of an embedding model on
that path is served as chat, and nothing refuses it. If that is your case, declare your own
InferenceServiceClass whose list names the single contract you need.
Additional administrator class (optional)
spec.acceleratorPolicy.allowedDeviceClasses is optional — it sets the allow-list of DeviceClass names for orders on that class. An order may narrow this allow-list with its own spec.resources.accelerator.deviceClasses (section 2 below); an empty intersection is refused.
apiVersion: ai.deckhouse.io/v1alpha1
kind: InferenceServiceClass
metadata:
name: llm-chat-shared
spec:
acceleratorPolicy:
allowedDeviceClasses:
- nvidia-h100-mps-mig
allowedSharingModes:
- Shared
allowedPlacementTypes:
- WholeDevice
maxAcceleratorCount: 1
minSharePercent: 25
maxSharePercent: 100
exposurePolicy:
type: External
authentication: TokenExpected: status.phase: Ready, conditions Validated=True, Ready=True.
2. InferenceService (namespace user)
apiVersion: ai.deckhouse.io/v1alpha1
kind: InferenceService
metadata:
name: support-llm
namespace: support
spec:
inferenceServiceClassName: default-llm
model:
ref:
name: Qwen/Qwen2.5-32B-Instruct-AWQ
src: HuggingFaceAn order is a class reference, a model, and an optional narrowing of the device class list. Its spec has no other fields.
The class reference is itself optional: an order that omits spec.inferenceServiceClassName
entirely skips the class-derived checks below — no allow-list intersection, no status.model.endpointType —
and gets exposure and admission built into the platform (the values the bundled default-llm class carries
on the day this phase shipped, not a live reference to that object). Every other policy already behaves the
same with no class as with a class that leaves the corresponding block unset.
An order may narrow the allow-list of the class with its own
spec.resources.accelerator.deviceClasses — the multi-element class list from the example above
lets an order pick one:
apiVersion: ai.deckhouse.io/v1alpha1
kind: InferenceService
metadata:
name: support-llm
namespace: support
spec:
inferenceServiceClassName: default-llm
model:
ref:
name: Qwen/Qwen2.5-32B-Instruct-AWQ
src: HuggingFace
resources:
accelerator:
deviceClasses:
- nvidia-h100-mps-migAn empty intersection with the class’s acceleratorPolicy.allowedDeviceClasses is refused with
reason ClaimDeviceMissing; a partial intersection is accepted, and the intersection — not the
order’s own list — is what travels to the launch plan.
The inference API contract is picked by the platform out of the allow-list of the
class intersected with the facts of the model: the only allowed contract, or the
first of several. The chosen value shows up in status.model.endpointType — the one
window the owner of an order has on that choice, because the class is not theirs to
read. An order that needs a different contract needs a different class.
The name of the runtime and every launch parameter come from the recipe of the launch
plan, compiled against the hardware the plan chose. MM catalog keys
(limitMmPerPrompt and similar) live there too. A different value is a different
recipe or a different class — an administrator’s decision.
A class must declare a non-empty allow-list of API contracts. A class without one is refused on write: the contract is picked from that list, an order names none, and a class without it would leave its orders with no contract at all.
An order that still carries a runtime block is rejected by a client asking for strict field validation, and silently pruned by one that does not.
Before persisting an object, the API server validates the model source union. It rejects: the
model.ref.namespace field on HuggingFace and ClusterModel references; ClusterLocal classes with https;
and certificate branches that do not match https.mode.
3. Controller behavior
- Validation: the class exists, is
Readyand declares its allow-list of API contracts;model.src=HuggingFace. - Build the order workload objects and apply them in namespace
support; every object is named after the order. - Compute
settings.domain(for examplesupport-llm.services.company.com) and publishstatus.endpoint. - When
authentication: Token, create Secret{name}-authand setstatus.authSecretName. - HTTP probe against the Inference API; set
phase: Readyonly after success. - After
Ready, periodic healthcheck; sustained API errors →phase: Failed, reasonServiceUnhealthy(bootstrap before firstReadystaysPending, reasonHealthCheckFailed).
4. Verify the result
Reading the status of a ready order is described once, in the User guide; the sections below say only what each scenario adds to that status.
5. Hugging Face token (optional)
For gated models, create a Secret in the order namespace and reference it in spec.model.authSecretRef:
spec:
model:
ref:
name: meta-llama/Llama-3.1-8B
src: HuggingFace
authSecretRef:
name: hf-token
key: token # optional; the default key is "token"The controller validates the Secret along with the order and mounts it into the model artifact fetcher of the order workload as HF_TOKEN.
RBAC
| Level / role | Resources | Verbs |
|---|---|---|
User (d8:user-authz:ai-inference:user, rbacv2 use/view) |
inferenceservices |
get, list, watch |
| User / use/view | inferenceserviceclasses/placement-preview |
create |
| User / use/view | inferenceserviceclasses/cluster-view |
get |
Editor (:editor, rbacv2 use/edit) |
inferenceservices |
create, update, patch, delete, deletecollection |
ClusterEditor (:cluster-editor) |
inferenceserviceclasses |
get, list, watch, create, update, patch, delete, deletecollection |
| ClusterEditor / manage | UI subresources above | create / get |
| manage/view | moduleconfigs/ai-inference, ISC |
get, list, watch (+ UI subresources) |
| manage/edit | moduleconfigs/ai-inference, ISC |
mutate (+ UI subresources) |
Intentionally denied to humans: inferenceserviceclasses/planner, */status, Secrets,
bind / escalate / impersonate. PrivilegedUser / Admin / ClusterAdmin deltas
are empty (no extra actions).
Templates: templates/user-authz-cluster-roles.yaml, templates/rbacv2/**.
Class policies
Example embeddings-dedicated class with admission/scaling/update and authentication: None:
apiVersion: ai.deckhouse.io/v1alpha1
kind: InferenceServiceClass
metadata:
name: embeddings-dedicated
spec:
admissionPolicy:
allowedNamespaces: ["*"]
exposurePolicy:
type: ClusterLocal
authentication: None
scalingPolicy:
allowedPriorityClassNames:
- demo-inference-high
minReplicas: 1
maxReplicas: 4
updatePolicy:
strategy: RollingUpdateAn order under that class names no replica bounds, so the bounds of the class above apply to it:
apiVersion: ai.deckhouse.io/v1alpha1
kind: InferenceService
metadata:
name: search-embeddings
namespace: ml
spec:
inferenceServiceClassName: embeddings-dedicated
model:
ref:
name: intfloat/multilingual-e5-large
src: HuggingFaceExpected when Ready: status.constraints.minReplicas and status.constraints.maxReplicas, an HPA with the replica bounds and a StatefulSet with its update strategy. With maxReplicas above one the condition ScalingHealthy reports what the HPA sees — true when healthy, false when growth is held. status.endpoint.type mirrors exposurePolicy.type (ClusterLocal / External). Which inference API contracts a class admits is decided by allowedEndpointTypes on the class, required and non-empty; an order names no contract itself. Refusal reason of this validation: NamespaceNotAllowed.
Integration with ai-models
Requires module ai-models with its internal lookup API when catalog
integration is enabled. Public ModuleConfig uses catalog.mode (Enabled by
default, or None to disable catalog clients). Internal transport defaults to
aiInference.aiModels.catalogTransport: Rest: controller and planner call
GET /api/internal/v1/models/lookup by model.ref. When Rest is unavailable,
they immediately read Model / ClusterModel CRs in the same reconcile.
Explicit rollback: internal catalogTransport: Kubernetes (CR-only). The
distribution and catalog-import APIs are not used for model-facts lookup.
ModuleConfig (public catalog mode)
apiVersion: deckhouse.io/v1alpha1
kind: ModuleConfig
metadata:
name: ai-inference
spec:
settings:
catalog:
mode: Enabled # None — disable catalog clients / RBACInternal catalog transport (not ModuleConfig)
In openapi/values.yaml (module internal values, not public ModuleConfig):
aiInference:
aiModels:
catalogTransport: Rest # default; Kubernetes — explicit rollback
catalogLookup: # used when catalogTransport is Rest
baseURL: https://ai-models-controller.d8-ai-models.svc.cluster.local:8080
path: /api/internal/v1/models/lookup
timeoutSeconds: 10
notReadyRequeueSeconds: 30InferenceServiceClass with modelPolicy
apiVersion: ai.deckhouse.io/v1alpha1
kind: InferenceServiceClass
metadata:
name: llm-chat-shared
spec:
acceleratorPolicy:
allowedDeviceClasses:
- nvidia-h100-mps-mig
allowedSharingModes:
- Shared
allowedPlacementTypes:
- Partition
maxAcceleratorCount: 1
admissionPolicy:
allowedNamespaces: ["*"]
modelPolicy:
allowedEndpointTypes:
- Chat
maxParameterCount: 70B
exposurePolicy:
type: External
authentication: Token
scalingPolicy:
allowedPriorityClassNames:
- demo-inference-normal
minReplicas: 1
maxReplicas: 1
updatePolicy:
strategy: RollingUpdateInferenceService with catalog model.ref
apiVersion: ai.deckhouse.io/v1alpha1
kind: InferenceService
metadata:
name: support-llm
namespace: support
spec:
inferenceServiceClassName: llm-chat-shared
model:
ref:
kind: ClusterModel
name: qwen2-5-32b-instruct-awq
src: ai-modelsFor a namespace Model, set kind: Model, name, and optional namespace in ref; the built workload does not carry the model namespace.
Controller behavior
- Validation of the class and its policies.
- Catalog fact lookup by
model.ref(default — over HTTP; when Rest is unavailable — immediate CR read; withKubernetes— CRstatusonly). - Local catalog matrix (
modelPolicyagainst the facts): on successModelResolved=True; otherwiseModelResolved=Falsewithreason(ModelNotFound,ModelNotReady, …). - The workload of the order is not built until
ModelResolved=True. - On success the platform builds the objects of the order itself; the API contract it selected is published in
status.model.endpointTypeand the scope of the model instatus.constraints.modelScope. - API health probe and
phase: Ready.
Verify status
When Ready, expect:
status.model.endpointType: Chat— the API contract the platform picked from the class allow-liststatus.constraints.modelScope: Cluster— from catalog factsstatus.endpoint.type: External— mirrorsexposurePolicy.type, not the API type- condition
ModelResolved: True
kubectl get inferenceservice support-llm -n support -o jsonpath='{.status.model.endpointType}{"\n"}{.status.endpoint.type}{"\n"}'
kubectl get inferenceservice support-llm -n support -o jsonpath='{.status.constraints.modelScope}{"\n"}'Resource planner
Builds on the catalog section above (model.ref, ModelResolved). Adds launch planning, condition Planned and status.resolved. Requires module gpu for the GPU inventory and the in-cluster resource planner Deployment shipped with ai-inference.
Compatibility: bundled default-llm and classes without acceleratorPolicy: the controller does not call the planner and does not publish Planned / status.resolved.
Prerequisites
- Ready
ClusterModel/Model, from the catalog section above. - Module
gpuwithPhysicalGPUinventory visible to the planner. - Planner Service reachable at the module default (
aiInference.planner.baseURLin chart values; seeopenapi/values.yaml).
InferenceServiceClass with acceleratorPolicy
Extend the llm-chat-shared class from the catalog section above with acceleratorPolicy (administrator manifest):
apiVersion: ai.deckhouse.io/v1alpha1
kind: InferenceServiceClass
metadata:
name: llm-chat-shared
spec:
acceleratorPolicy:
allowedDeviceClasses:
- nvidia-h100-mps-mig
admissionPolicy:
allowedNamespaces: ["*"]
modelPolicy:
allowedEndpointTypes:
- Chat
maxParameterCount: 70B
acceleratorPolicy:
allowedSharingModes: [Shared]
allowedPlacementTypes: [Partition, WholeDevice]
maxAcceleratorCount: 1
exposurePolicy:
type: External
authentication: Token
scalingPolicy:
allowedPriorityClassNames:
- demo-inference-normal
minReplicas: 1
maxReplicas: 4
updatePolicy:
strategy: RollingUpdateExpected: status.phase: Ready.
InferenceService with accelerator and launchStrategy
apiVersion: ai.deckhouse.io/v1alpha1
kind: InferenceService
metadata:
name: support-llm
namespace: support
spec:
inferenceServiceClassName: llm-chat-shared
model:
ref:
kind: ClusterModel
name: qwen2-5-32b-instruct-awq
src: ai-models
launchStrategy: ThroughputlaunchStrategy selects the compiled recipe branch (Latency, Throughput, or Balance; default Latency when omitted). The branches genuinely differ on the vast majority of hardware — a wider context horizon or a throughput-oriented balance is reachable through this field, not by changing the recipe or class as a whole. The count of devices is not an order field — the launch plan assigns it within acceleratorPolicy.maxAcceleratorCount of the class.
The share of a device is ASSIGNED by the launch plan, and the order names none. The plan takes the
smallest whole percent that admits the memory the model needs on the chosen device, clamps it into
minSharePercent/maxSharePercent of the class, and falls back to a whole device when the class permits no
sharing, when the chosen placement is not a whole device in a sharing mode, or when either memory figure is
unknown. The assigned value is published in status.resolved.sharePercent, so the owner of an order sees
how much of the card is theirs. A share below 100 also requires the selected DRA device to publish
allowMultipleAllocations with a sharePercent request policy. For a share of 1..99 the controller sends
capacity.requests.memory from the model requirement of the launch plan, written into the claim as
gpu.deckhouse.io/memory. Model memory and the MPS compute share are accounted independently.
Controller behavior
- Validation and the catalog matrix →
ModelResolved=True. - The accelerator policy of the class bounds the outcome of planning; the order has nothing of its own to check against it.
POST /api/v1/launch-plan— the planner returns placement + runtime recipe; on success:status.resolved— the placement the order runs on:deviceClass,acceleratorProductName,acceleratorMemoryGiB,placementMode,sharingMode,sharePercent; everything else the planner computed travels in the launch plan of the order and is not published in the status;- condition
Planned=True, reasonLaunchPlanCalculated.
- On planner refusal —
Planned=Falsewith stable reason (NoCapacity,QuantizationMismatch, …); the workload objects are not created or updated until planning succeeds. - After
Planned=Truethe recipe from the plan is applied into the arguments of theruntimecontainer; then the health probe →phase: Ready, as in the sections above. - When
maxReplicas > 1— HPA observation and the background preemption loop may adjust donormaxReplicas; the bundleddefault-llmclass is out of scope.
Verify status
While reconciling, expect Planned before the workload of the order exists. When Ready:
status:
phase: Ready
resolved:
deviceClass: nvidia-hopper-s3-shared
acceleratorProductName: NVIDIA H100 80GB HBM3
acceleratorMemoryGiB: 24
placementMode: Partition
sharingMode: Shared
replanCount: 0
conditions:
- type: ModelResolved
status: "True"
- type: Planned
status: "True"
reason: LaunchPlanCalculated
- type: Ready
status: "True"kubectl get inferenceservice support-llm -n support -o jsonpath='{.status.resolved}{"\n"}{.status.conditions[?(@.type=="Planned")]}{"\n"}'
kubectl get statefulset support-llm -n support -o jsonpath='{.spec.template.spec.containers[0].command}{"\n"}'Planner refusal examples: Planned=False, reason: NoCapacity or QuantizationMismatch.
Planner runtime fallback
Builds on the resource planner section above. When there is no entry in runtime-recipes.compiled.yaml for (family, model.name) and runtime fallback is enabled, the planner synthesizes a generic vLLM recipe from the images/catalogs/ tables (recipe-taxonomy.yaml and platform-hardware-families.yaml), estimates vramMinimumGiB, and returns recipeSource: fallback. Compiled presets and recipes always win over the fallback path.
Compatibility: with aiInference.planner.runtimeFallback.enabled: false (chart values / module internal config), behavior matches the resource planner path above — no synthetic recipe. Classes without acceleratorPolicy are unchanged.
Module settings
In openapi/values.yaml (not public ModuleConfig):
aiInference:
planner:
runtimeFallback:
enabled: true # default trueThe controller forwards the flag to the planner as runtimeFallback.enabled. To disable:
aiInference:
planner:
runtimeFallback:
enabled: falseMinimum metadata for fallback
| Field | Source |
|---|---|
parameterCount on the direct HuggingFace path |
spec.model.parameterCount → parse model.name (32b, 360m, …) |
parameterCount on the ai-models path |
available catalog fact → parse model.name (32b, 360m, …). The order cannot name the size on this path at all: spec.model.parameterCount is refused on write |
quantization |
catalog → parse suffix in model.name (awq, nvfp4, …) → default |
format, supportedEndpointTypes |
catalog (controller forwards; not required for VRAM estimate v1) |
The controller does not parse quantization from the name — only the planner on the fallback branch. For modelPolicy.maxParameterCount, the controller judges whatever the path of the order resolved: the catalog fact on the ai-models path, the order value on the direct one, and the same model.name parsing when that first rung is empty.
Model and ClusterModel lookup facts do not return tensor geometry today. The
planner gets geometry from a shipped recipe preset when one exists; otherwise
the fallback estimate is geometry-blind. Publishing geometry from ai-models
is not available today, and this module does not assume future catalog fields.
Example: model without a compiled recipe
ClusterModel named qwen2-5-32b-instruct-awq whose lookup facts omit
parameterCount and quantization. The order also omits
spec.model.parameterCount, so size and quantization come from model.name:
apiVersion: ai.deckhouse.io/v1alpha1
kind: InferenceService
metadata:
name: support-llm-fallback
namespace: support
spec:
inferenceServiceClassName: llm-chat-shared
model:
ref:
kind: ClusterModel
name: qwen2-5-32b-instruct-awq
src: ai-modelsExpected once the GPU inventory is populated:
status:
resolved:
deviceClass: nvidia-hopper-s3-shared
acceleratorProductName: NVIDIA H100 80GB HBM3
acceleratorMemoryGiB: 24
placementMode: Partition
sharingMode: Shared
conditions:
- type: Planned
status: "True"
reason: LaunchPlanCalculatedWithout a recognizable size (no catalog, no spec.model.parameterCount, name without 32b/360m token) — Planned=False, reason NoCompatibleRuntimeAvailable.
Verify status
kubectl get inferenceservice support-llm-fallback -n support \
-o jsonpath='{.status.resolved.deviceClass}{"\n"}{.status.resolved.acceleratorProductName}{"\n"}'
kubectl get statefulset support-llm-fallback -n support \
-o jsonpath='{.spec.template.spec.containers[0].command}{"\n"}'