The module lifecycle stage: Experimental
The module has requirements for installation
Product: ai-inference (Deckhouse Kubernetes Platform module)
Stage: Experimental
Date: 2026-08-05
Platform compatibility
| Platform | Supported floor |
|---|---|
| Deckhouse Kubernetes Platform | 1.75 |
| Kubernetes | 1.34 |
The core resource.k8s.io/v1 DRA inventory is supported at that floor. Consumable-capacity
fields remain optional: when the GPU driver does not publish them, the planner does not invent
fractional capacity. This can reduce placement density, but prevents GPU overcommit.
This page describes the current experimental contour for operators and namespace
users. It is the documentation canon for release notes. The Console feed is a
short features / fixes projection of the same meaning.
Highlights
- Order an inference workload with cluster
InferenceServiceClassand namespacedInferenceService; the controller deploys the objects of an order itself and publishesstatus.endpoint. - An order is a class reference and a model. The inference API contract (Chat, Embeddings, Rerank and related) is picked by the platform from the allow-list of the class; the runtime configuration comes from the recipe of the launch plan.
- Plan placement and preemption through the planner API; autoscaling and donor restore stay under module ownership.
New features
- Bundled class and walkthrough path for a first Ready order.
- Placement preview, launch plan, preemption and free-replicas planner routes for operators and UI clients.
- Optional device-class and override surfaces on the order for GPU claim shaping.
Improvements
- Status conditions and readiness projection clarify order progress without exposing the platform’s internal churn as user contract.
- Shared estimate helpers keep host memory, CPU and PVC disk sizing consistent across controller and planner paths.
- The delivery image names the system packages whose files it carries, and carries their licences. Some files reached the image by copying, without the package records they belong to: the vulnerability scan did not see such files at all — not because it found them clean, but because it did not know they were there. The delivery now declares those packages by their records and carries the licence text of each at its canonical path, and no image leaves until the licence text of every declared package has been found.
Fixes
- The node resource estimate no longer charges the order for the weights of the model. The weights live on the accelerator and are already paid for as the VRAM requirement; on the node they are read as a stream through a page cache the kernel reclaims. Measured on an order of a hundred and twenty billion parameters: 66 GiB were assigned while the working set of the replica was 3663 MiB. The CPU estimate was read against the parameter count, which the work of the node barely follows — the accelerator computes, the node loads the weights and captures graphs — and eight cores were assigned against a steady draw of 0.083 of a core. The node now holds the runtime and the bounded window of its loader, and the CPU estimate follows the bytes read while loading. Above the small-model threshold no estimate yields a single core: the request receiver and the compute loop are separate processes, and on one core they take enough time from each other for the liveness probe to kill a healthy replica. The floor of vertical advice is read off the same estimate rather than a constant: advice may ask for MORE than the plan sized and never for less — it used to substitute its own, and an order received one core out of eight. Across a catalog of 363 models the sum of memory estimates fell from 17,644 to 2454 GiB with no model rising, and the largest estimate from 1268 to 14 GiB; fourteen models used to ask for more node memory than the largest node of the stand has.
- An order answering requests is no longer declared not ready because it is short of
capacity to GROW. Recomputing the plan of such an order can be refused — when it needs
more devices than it holds and none are free, say: the driver withdraws an occupied
device from publication. That refusal used to
put the order into
Pending, end the reconcile before the objects of the order were built back, and erase the very condition by which the next pass decided whether to compute a plan at all — the order stayed not ready while it served, and never came out of it by itself. An order whose replicas hold their floor and their device now staysReady: thePlannedcondition holds true with the reasonLaunchPlanInForce, and the refusal, with its own words, sits in themessageof that condition. - Recomputing the plan of a serving order no longer trips over the order’s own place. The
driver does not publish a device a claim has already selected, and the module used to
drop the order’s own entry from the register of occupied capacity — the holder of the
only device that fits stopped being visible to itself and was refused
NoAcceleratorWithEnoughMemorywhile a replica served. The capacity an order holds is now handed to the planner apart and takes part in placing that order as free; a partition of an accelerator is recognized on a par with a whole card. The memory refusal also names the required amount and the largest one found, so «the model is too large for the fleet» and «the fleet shows nothing large right now» are told apart from one message. - A device claim that could not be placed for a replica the order does not need no longer
walks the order off the device class it runs on. Such a claim used to start a round of
replans on behalf of the WHOLE order: the class landed in the register of excluded ones
and the order was walked off its own place — and another place cannot be found for it
without taking the set of replicas down, that is, without losing a working replica for
one that does not exist yet. The subject of the round is now the claim the floor of the
order needs; capacity missing for growth is announced as a
GrowthClaimUnplacedevent on the order and leaves it on its class. A claim whose device reports a failure does not count as placed, so a broken device under a replica of the floor starts the round as before. - An order pinned to a MIG partition device class held by a single order is no longer
refused with no way forward. Every partition profile in the hardware catalog pinned the
shared sharing mode, while a partition device with no sharing attribute is identified as
dedicated, so such a slot matched no profile at all and placement answered
NoCompatiblePlacementon healthy hardware with memory to spare. Partition profiles no longer pin a sharing mode: one description now serves both kinds of slot, and the memory reserve for sharing a partition’s compute is subtracted from the slot’s actual mode — the shared slot still pays it, the dedicated one does not. - Placement no longer answers differently for the same cluster from one snapshot to the next when a partition is published both as a dedicated and as a shared device class. Such a pair used to be able to tie in the candidate comparison, and a tie was resolved by whichever slot the inventory happened to return first.
- An order asking for a context horizon above the model’s window is now refused by name
instead of diverging.
runtime.<endpointType>.maxModelLenwas checked only against the ai-models catalog fact, so on the direct Hugging Face path nothing bounded it. The planner then sized the VRAM budget for the clamped horizon while the pod was launched with the requested one, and the runtime failed to start with no reason pointing at the order. The check now runs for both model sources and against the narrower of the two windows the platform knows — the catalog fact and the recipe preset — and the refusal message names which of them bound the decision. - An uncalibrated inference runtime no longer receives an invented VRAM overhead. An unrecognized runtime name silently got 0.5 GiB, which under-sizes the budget; it is now a named refusal. No shipped asset can reach this path today, so the change is defensive.
- The minimal order now works on the bundled class. It declared no
modelPolicy, so an order that named only a class and a model was refused withEndpointTypeAmbiguous— a reason that is not retried, leaving the orderFaileduntil edited by hand. The bundled class now declaresmodelPolicy.allowedEndpointTypes: [Chat], so the single allowed type is selected automatically and the two-reference order reachesReady. - A model size stated on the order can no longer understate the catalog. The effective
size used to be
min(order, catalog), so an order could only ever lower it — which both slipped pastmodelPolicy.maxParameterCountand undersized the memory budget handed to the planner. When the catalog holds a fact about the model, that fact is now the effective size; the order value applies only where no fact exists. - Pod memory and CPU come from one estimate — the one the planner computed and
put into the launch plan of the order. The bridge used to compute
them separately and missed the CPU KV cache headroom a recipe asks for: for
google-gemma-4-*models onintel-cputhe memory request grows by the 40 GiB the recipe declares, and thevpa.resourcePolicy.maxAllowedceiling doubles that. Pinningresourceson the order still wins over both sources. - Recipe environment variables are no longer dropped wholesale. A boolean recipe
key was written into status as
true/falsewhile the reader expected1/0, and the failure was replaced with an empty value — the pod lost its entireenv, including variables set on the order. A value the catalog cannot read now refuses with reasonRuntimeParameterNotSupportedinstead of launching without the variables. - Preset context horizons now match the model window. For
microsoft-phi-3-5-mini-instructthe window itself was understated — declared 4096 against a real 131072 — so the pod served 4096 instead of 8192; the window is curated and the horizon now reaches the pod in full. Formicrosoft-phi-3-mini-4k-instructandmicrosoft-phi-3-medium-4k-instructthe 4096 window is correct and the horizon was lowered to it; behaviour of those two models does not change.
Upgrade notes
-
The platform no longer keeps a list of accelerator names. The kind of hardware is derived from what the device says about itself — the generation from the resource snapshot, else a prefix of the product name, else a refusal. Every device of a declared family becomes orderable, not only one whose name somebody had written down. One thing is required of you, and only if you read the
profileIdfield of a launch plan or of a placement preview: the name of a derived record spells the full published name of the device where the withdrawn record spelled a shortened marketing one. Measured on the withdrawn list: of forty-seven partition records the spelling moved for eighteen —a100-40gbbecamea100-sxm4-40gb,gb200becamegb200-nvl4. The order status carries no such field, and nothing else observable changed. -
An order placed before the upgrade on an older platform re-plans once. Such an order keeps in its status the pattern of the record it was placed by rather than the name of a device; a pattern no longer resolves, the order re-plans and its status is rewritten with the name of the card. No manual step is needed.
-
The cluster view names the hardware kind of every accelerator. The accelerator identity gained
identity.hardwareKey— the kind the platform assigns to this card. It is empty when the rules lead nowhere: such a card cannot be ordered, and the view says so rather than staying silent. The resolution of a kind also began to judge a fact rather than a spelling: before this, a card naming a generation that happens to spell out a key reached a kind it never named. Nothing is required of you: the identity fields you already read are unchanged, and a reader that does not know the new field behaves as before. -
An order on hardware the wheel carries no kernels for is refused before it starts. A device names its own compute capability, the family register declares the capabilities the kernels are compiled for, and the platform compares them. Such an order used to be accepted and to die at start: the binary of one capability does not run on a device of another. One thing is required of you: an order refused
NoCompatiblePlacementon hardware that used to be accepted does not need a retry but support for that capability in the delivery, which is declared by the source contract of the runtime. A limit is worth knowing too: a device whose capability the snapshot does not name is judged as before — by its kind of hardware alone — so an installation with an older resource driver does not change behaviour. -
The platform now recognizes hardware by what the hardware says about itself. A device whose generation the resource snapshot names gets its kind of hardware without an entry in the register of names. Hardware that used to be refused becomes orderable: measured,
NVIDIA H20,NVIDIA H800,NVIDIA A16,NVIDIA A2,NVIDIA L2,NVIDIA L20,NVIDIA GeForce RTX 4090,NVIDIA RTX 6000 Ada GenerationandAMD Instinct MI300Xmatched no entry of the register at all, although their families were declared supported. Entries of the register still win over the derivation, so not a single kind recognized before has changed. One thing is required of you: an Intel accelerator the platform does not recognize now gets aNoCompatiblePlacementrefusal rather than the kind of a central processor — the old substitution handed an order a recipe compiled for other hardware. A limit is worth knowing too: the platform does not know what share of the devices in the field publish a generation, so the kind is derived by a ladder — the generation, else the product-name rule, else a refusal — and hardware no step of the ladder reaches is refused before it starts. -
The accelerator name in the status of an order is now the name of the card. The field
status.resolved.acceleratorProductNameused to carry the pattern of a hardware record rather than the name of a device: an order placed onNVIDIA H100 PCIereportedNVIDIA H100*. It now carries what the device calls itself, and there is no star in the value any more. The same holds forstatus.resolved.acceleratorVendorand for the name in the placement preview. One thing is required of you: if you have a watcher comparing this value against a string with a star, rewrite the comparison against the name of the card. A second thing is worth knowing: donor selection for preemption compares this name, so a target and a donor on different cards of one kind (sayNVIDIA H100 PCIeandNVIDIA H100 80GB HBM3) are no longer treated as compatible — they used to be, and the target was offered a donor whose freed card it cannot use. An order planned before this release is not offered as a donor until its placement is computed again; that happens on a change of the device inventory or an edit of the order. -
Orders will ask the node for different resources. The estimate is recomputed on the first pass after the upgrade, and the new figures reach a replica when that replica is next replaced — a scale-out, an edit of the order, a lost node. Memory moves one way only: 166 models of the catalog fall, 197 are unchanged, none rise. Cores move both ways: 95 models fall and 43 gain one core, always the step from one to two. Nothing is required of you, but a node packed to its CPU limit with orders from those forty-three will hold fewer of them; the sum of core estimates across the catalog falls from 864 to 639.
-
The
Plannedcondition is now true in two ways. Besides the familiarLaunchPlanCalculated— a plan computed by this pass — there isLaunchPlanInForce: the order is assembled from the plan it already holds, and recomputing that plan was refused. Nothing is required of you. One thing is worth knowing: a watcher that picked out orders with no place byPlanned=Falsewill no longer find a serving order whose recomputation was refused — such an order is nowPlanned=True, and the refusal sits in themessageof the condition and in the counter of validation failures. -
A new runtime release no longer restarts the replicas that are serving. An upgrade that brings a new runtime release carries it into the description of the workload on the first reconcile, and the replicas already running are left on the release they were started with. A replica raised after that — a widened set, a replacement after a node went away — comes up on the shipped release. While the two differ the order carries
RuntimeCurrent=False, reasonReplicasHeldOnPreviousRuntime, and the message names both releases and how many replicas reached the shipped one. Nothing is required of you; editing the order moves every replica the way its class asks, and deleting a replica moves that one. Two things are worth knowing: while the set is mixed the replicas answer differently near the context length and the concurrency limit, because those come from the recipe of each replica’s own release; and if the recomputed plan moves the placement of the order — another device class, another share, a larger model volume — the replicas cannot be kept at all, the order reports reasonWorkloadRebuildingand goes throughPending. Before this release the shipped runtime never reached a steadyReadyorder at all: it arrived at whatever later moment something else touched the order, which is to say the restart happened outside any upgrade window. SeeADMIN_GUIDE, «Upgrading the module». -
Breaking.
updatePolicy.strategy: Recreateis no longer accepted. The value named nothing the workload object of an order knows — a StatefulSet takesRollingUpdateorOnDelete— so a class declaring it had every apply of every workload of its orders refused, and the refusal reached the owner of the order rather than the author of the class. The schema now acceptsRollingUpdateonly, and a class stored earlier withRecreateis refused by validation with the strategy named in the reason. Fix such a class before upgrading; orders of classes namingRollingUpdate, or no update policy at all, are untouched. -
Breaking. The bundled class is renamed, and an order naming the old name loses its class. The bundled
InferenceServiceClassis nowdefault-llm; the old name wasdefault-llm-chat. Helm creates the object under the new name and removes the old one, so an order whosespec.inferenceServiceClassNamestill names the old one reports that its class does not exist. Replace the name in your orders before upgrading: nothing migrates the references for you, and there is no grace period in which both names answer. What you gain by it. The same class now permits every inference API contract the module supports — chat, embeddings and reranking — instead of chat alone. An order names no contract; the platform picks it from the list of the class, narrowed by what the source of the model says the model can serve. So on the catalog path an embedding model is served as an embedding model, with no class of your own. What still needs a class of your own. On a source that carries no facts about the model, nothing narrows the list and the FIRST contract of it wins — chat. An order of an embedding model on such a source is served as chat, silently. If that is your case, declare a class whose list names the one contract you need. -
Breaking, and the transition is dangerous by silence. Two fields leave the spec of an order:
spec.launchStrategyandspec.priorityClassName. The schema of an order is structural and keeps no unknown fields, and the admission policy on write checks only the length of a name — so an order still naming those fields is accepted while the fields vanish from the stored object without a word. There will be no error by which an owner would learn about the transition: fix your manifests before upgrading. Where to get the answer. The branch of the compiled recipe cannot be set anywhere: the platform reads the latency branch for every order. The priority of a workload comes from the classallowedPriorityClassNamesallow-list, and the chosen name is visible instatus.constraints.priorityClassName. -
Breaking. Three sub-blocks leave the resources of an order:
spec.resources.requests,spec.resources.limitsandspec.resources.storage. The platform ESTIMATES the CPU and memory of the inference container and the floor of the model volume from the size and the quantization of the model, and a value named on the order used to cancel that estimate whole — one key of cpu or memory switched the whole overlay off. The applied numbers are visible on the workload of the order. The size of the volume is the sharpest of the three: a volume cannot be resized afterwards, so an order sized by hand stayed wrong for ever. -
The StorageClass of the model volume moves to the class. Nobody computes where a model should lie, so the decision belongs to an administrator:
spec.modelStorageClassNameonInferenceServiceClass. A class that names none leaves the volume to the default StorageClass of the cluster, and the platform adds no second default. -
A volume that already stands keeps its size, as long as that size still serves the order. The size is settled when the volume is CREATED and cannot be changed afterwards — the server refuses the whole apply of a workload naming another size. So the platform reads the standing workload and carries its claim templates forward instead of computing them again. Which way the divergence goes decides what happens:
- the volume is larger than the estimate — it is kept and the order stays ready. Nothing is asked of
you, no claim is resized or deleted, and the order gets a
SettledRegionKeptevent naming both sizes. The claim templates travel whole, so amodelStorageClassNamethe class has since changed is kept as well: a volume does not move to another StorageClass, and a standing order keeps the one it was made on; - the order has outgrown the volume — the model no longer fits, so nothing is carried and the order
is refused rather than reported ready on a volume it cannot use. The order gets a
SettledRegionTooSmallwarning naming both sizes; the server’s own refusal only says that the field is forbidden. To get the order moving, delete BOTH its workload and the claim behind it — for the orderchatthat is the StatefulSetchatand the PersistentVolumeClaimstorage-chat-0. The next pass builds the workload back at the size the order now needs, and the artefact is downloaded again. Deleting the workload alone is not enough and leaves the order worse off: the claim outlives it, the rebuilt workload binds to that same claim, and the order then runs on the old volume with no warning at all — the two claim templates now agree, so there is nothing left to report.
- the volume is larger than the estimate — it is kept and the order stays ready. Nothing is asked of
you, no claim is resized or deleted, and the order gets a
-
How the removals above are refused, measured rather than assumed. A client that asks for strict field validation — which
kubectl applydoes by default — gets a manifest naming any removed field REFUSED, and the message names the field. A client that does not ask for it gets the object accepted with the field pruned away in silence. Both are true; which one you meet depends on your client, and automation on a client library is the half that gets the silence. -
Breaking, with a DELAYED consequence. The block of replica bounds leaves the spec of an order:
spec.scaling. As with the fields above, an order still naming it is accepted and the block vanishes from the stored object without a word — but here the effect is not immediate. An order working in three replicas keeps them until the bounds are recomputed, and only then drops to what its class allows. The moment of that recompute is not something you can read off the object, so fix your manifests rather than watch for it. Where to get the answer. The bounds come fromspec.scalingPolicyof the class, and the effective values are visible instatus.constraints.minReplicasandstatus.constraints.maxReplicasof your own order. An order that needs its own bounds needs its own class, and that is an administrator’s decision. -
A class without a scaling policy gives one replica. This is declared behaviour now, not the leftover of an unset field: an order names no bounds, and the platform has no second source. For administrators: a class under which orders have to work in more than one replica must declare
spec.scalingPolicy. The bundled class receives it in this release (one to two replicas). -
The refusal
ScalingOutOfBoundsis gone, because nothing of the order is left to compare with the policy of its class. A class whose own policy crosses its bounds is still refused — as the fault of the class, underInvalidServiceClass. -
Breaking, with a DELAYED consequence, and one capability moves from the order to the class. The accelerator sub-block leaves the spec of an order, and the shell of the resources block goes with it:
spec.resources.accelerator.count,spec.resources.accelerator.sharePercent,spec.resources.accelerator.deviceClassesandspec.resourcesitself. A strict client is refused namingspec.resources; a lenient one is accepted with the block dropped in silence. The consequence is delayed: an order running on two devices keeps them until the next planning, and only then gets what the plan assigns. Where to get the answer. The launch plan assigns the count of devices and the share of one for the chosen model on the chosen hardware, inside the limits ofspec.acceleratorPolicyof the class. The assigned share is visible instatus.resolved.sharePercentof your own order, and the assigned device class instatus.resolved.deviceClass. The allow-list of device classes is the one of the class, and an order that needs its own needs its own class. -
Two refusals are gone, because neither has a subject any more:
AcceleratorCountNotAllowedandAcceleratorShareNotAllowed. The limits of the class did not change and did not weaken — they now bound what planning assigns instead of what an order asked for. A bound that does not admit the memory a model needs gives a whole device rather than a refusal. -
Orders that stood refused will START WORKING. An order asking for more devices, or for a share outside the bounds of its class, was refused and stayed refused. There is nothing left to ask with, so such an order now goes through and gets what the plan assigns. If you kept one of those refusals as a guard against an order you did not want running, the guard is gone: use the policies of a class of its own instead.
-
A class without an accelerator policy gives a whole card. Splitting a card is permitted by the class and by nothing else now: an order asks for no share. For administrators: a class whose orders should share accelerators must declare
spec.acceleratorPolicyallowingSharedtogether withWholeDevice. The bundled class receives such a policy in this release. -
The result: an order is a class reference and a model. The runtime block leaves the spec of an order whole — the API contract and the name of the runtime were its last two fields. A strict client is refused naming
spec.runtime; a lenient one is accepted with the block dropped in silence. Where to get the answer. The 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 instatus.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 is picked by the recipe. -
A second breaking change, and it is on the other surface: a class must declare a non-empty allow-list of API contracts. A class without one is refused on write, and the message names the field. It is a refusal addressed to the administrator rather than to the owner of an order: 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. Hand-written classes without the list will stop being accepted on their next edit. Declare
spec.modelPolicy.allowedEndpointTypeswith at least one contract. The bundled class declares it, and so does every class shown in the examples, so the edit only touches hand-written ones. -
The refusal about an ambiguous API contract is gone together with the order field that used to rescue it. An order under a class with no list used to be refused in a way its owner could not fix. Such a class is now refused on write, and a class stored before the requirement refuses the order as a fault of the class.
-
Configuring the runtime from an order is no longer possible — the capability is withdrawn in full. Three parameter sections leave the spec of an order:
spec.runtime.chat,spec.runtime.embeddingsandspec.runtime.rerank, with every field they carried — the context length, the number of concurrent sequences, the KV cache dtype, the output token limit, the temperature, the nucleus fraction, trusting remote model code and the pooling kind. A strict client is refused naming the section; a lenient one is accepted with the section dropped in silence. Where to get the answer. All of these values are set by the recipe of the launch plan, compiled against the hardware that was chosen: the context length and the sequence count depend on the memory of the card, and the cache dtype on what the hardware supports. A different value is a different recipe or a different class — an administrator’s decision. -
Breaking, and this removal GIVES a value back rather than taking one away. The process environment leaves the spec of an order:
spec.runtime.env, with its two keyscpuKvcacheSpaceEnvandlogLevelEnv. A strict client is refused namingspec.runtime.env; a lenient one is accepted with the object dropped in silence. Every environment variable of the inference process now comes from the recipe of the launch plan, and the order of precedence is two rungs instead of three: the recipe, then the internal defaults of the module. Where to get the answer. A value that has to be different is set by the recipe. An order with its own requirements for the environment needs its own recipe or its own class, and that is an administrator’s decision. -
The key you REMOVED comes back. This is the sharp edge of both removals — the parameter sections and the environment object — and it has no analogue among the ones above. An order could drop a key inherited from the recipe by writing an explicit
nullinto it; with the section and the object gone there is nothing to drop it with, so the recipe value returns. The key does not disappear from the pod — it APPEARS in it. If you were suppressing a recipe value that way, the pod will run with it on the next assembly of the workload. -
The refusals
LegacyExtraEnvandLegacyExtraArgsare gone. Each caught a manifest still carrying a legacy array — of environment variables or of launch arguments — and pointed at a migration target: the closed environment object and the parameter section. Neither exists any more, so the refusals could only point at nothing. Such a manifest is now refused by the schema itself, as an undeclared field. -
The refusal
RuntimeParameterNotSupportedstays, but its subject is a different one now. It no longer concerns an order: there is nothing of an order left to check. It names a key of the RECIPE that the parameter catalog cannot read — and the order is refused rather than launching a pod on the runtime’s own defaults. -
A narrowed capability. A throughput order can no longer be placed: the
ThroughputandBalancebranches are not selectable by an order. Compiled recipe entries still carry those branches and the branch key of the launch-plan request is unchanged — an order simply no longer sets it. -
Priority now differs between classes, not within one. Two orders of one class used to differ in priority through the order’s own field; the priority now comes from the class allow-list, so two orders of one class are equal in priority and neither can be a donor for the other. Different priorities are expressed by different classes.
-
Breaking: a refusal on write for catalog orders. The size of a model (
spec.model.parameterCount) and the credentials reference (spec.model.authSecretRef) are accepted only when the source isHuggingFace. An order through the catalog carrying either is refused on write, and the message names exactly the field that does not belong. The size comes from the facts of the catalog entry; the credentials for fetching a catalog artifact live with the catalog itself. On the direct path both fields work as before. -
Breaking. The composition of
status.resolvedon an order is now closed: it keeps the placement the order runs on —deviceClass,acceleratorProductName,acceleratorMemoryGiB,placementMode,sharingMode— and the state of the planning episode:replanCount,excludedDeviceClasses. Thirteen fields are gone:profileId,hardwareKey,quantization,acceleratorVendor,vramRequiredGiB,acceleratorCount,launchStrategy,recipeSource,recipeConfirmed,modelStorageGiB,hostMemoryGiB,hostCPUCores,deviceClassesNotAllowed, and with them the nestedruntimeblock. The wholestatus.appliedClassblock is gone as well. An expression addressing a removed path returns an empty value rather than an error, so check your queries before upgrading. Where to get the answer if you needed one: the name of the applied class is inspec.inferenceServiceClassNameof the same order, where you set it; the scaling policy of the class is on the class, for those allowed to read it; the effective replica bounds, the priority and the model scope stay on their paths instatus.constraintsandstatus.model. The remaining removed values are internal: the planner computes them and the reconcile of the order takes them from the launch plan. They had no external reader, and the module will not publish them again — the surface of an order answers “where did my order land and am I alone on the hardware”, it does not restate the answer of the planner. -
Breaking. The
spec.settings.maassettings block leaves the module’s values schema, together with itspackageRepositoryNameandpackageVersionkeys. A key the schema no longer declares is rejected by the API server: aModuleConfigthat sets it will not apply after the upgrade. Remove themaasblock from the module settings before upgrading. There is nothing to put in its place and nothing is needed: what the repository and version were pinned for, the module now does itself — it builds the images of the request runtime and of the model artifact fetch and names them by digest, and it deploys the objects of an order without an intermediary. -
An order that names the device class
maasnow gets exactly that class. The name used to read as «no device class named» — it was the default value of the foreign chart — and the order was resolved as if it had asked for none. If you have a device class of that name and an order for it, the order will land on it after the upgrade. -
Breaking. The readiness condition of an order is renamed from
ApplicationReadytoWorkloadReady, and its two failure reasons fromApplicationNotReadyandApplicationErrortoWorkloadNotReadyandWorkloadError. Replace the three names before upgrading; the same applies to queries overai_inference_validation_failures_totalthat select the old reason names. Nothing has to be re-created: an order already in the cluster carries the new condition after its first reconcile on the new version, and the same reconcile removes the entry of the old condition type fromstatus.conditions. Until it runs, that entry is still there holding the value it last had —Truefor an order that was working — so automation must not read it while waiting for the upgrade to roll out. -
Breaking. The scaling condition of an order is renamed from
ScalingLimitedtoScalingHealthy, and its polarity is reversed:Truenow means healthy scaling,Falsemeans a scaling limit. The old name is not gone from the contract — it became the REASON on the negative side, besidePreempted, and the condition of the Kubernetes HPA keeps its own name and its own meaning. Renaming alone is not enough: every check of the status has to be turned over too. An expression that looked forTrueas the sign of trouble must now look forFalse, and one left as it was returns the healthy orders instead of the limited ones — with no error to show for it. Nothing has to be re-created: an order already in the cluster carries the new condition after its first reconcile on the new version, and the same reconcile removes the entry of the old condition type fromstatus.conditions. Until it runs, that entry is still there holding the value it last had —Falsefor an order that was working, which reads as a refusal under the new polarity — so automation must not read it while waiting for the upgrade to roll out. -
The module now deploys the objects of an order itself instead of asking a delivery package to do it, and their names lose the
-appsuffix: the orderchatis served by the workloadchat, notchat-app. This is not a field of the contract, but it is visible to anything that reached the objects of an order by name — dashboards, metric queries, hand-writtenkubectl. Theapplabel on the pods carries the same new name. -
The workload of the old path stays in the cluster, and removing it is yours. The module no longer holds the permission to touch it and no longer knows its name: it deploys and prunes only the objects it builds itself, and an order is not re-created by an upgrade, so nothing garbage-collects the old set either. The old
{order}-appworkload therefore keeps running beside the new one, and its pods keep holding the device claim they were given. On a limited device pool that matters twice over: the devices are consumed by two workloads instead of one, and the new claim of an order may find nothing left to allocate — an order that wasReadybefore the upgrade can stayPendingafter it. Remove the leftover set per order after the upgrade, checking first that the new workload serves the order. Go by the object NAME, not by a label: the old set and the new one carry the sameapplabel — the name of the order — so a label selector cannot tell them apart. The old objects are the ones whose name ends in-app:kubectl -n <namespace> get inferenceservice <order> -o jsonpath='{.status.phase}' kubectl -n <namespace> get statefulset,service,ingress,poddisruptionbudget,secret,\ resourceclaimtemplate,servicemonitor,horizontalpodautoscaler,verticalpodautoscaler,certificate \ -o name | grep -- '-app$' kubectl -n <namespace> delete application <order> --ignore-not-foundDeleting the
Applicationobject is not enough on its own — the rendered objects carry no owner reference back to it — so remove what the second command listed as well, once you have read the list. -
spec.model.parameterCountandInferenceServiceClass.spec.modelPolicy.maxParameterCountnow require an explicitMorBsuffix (case-insensitive). Manifests that use a bare value such as"32"are rejected by the API server after the upgrade. Replace each bare value with the intended scale, for example32Bor350M, before upgrading the module. -
Preemption now reads the scheduler’s own verdict when it counts pods blocked on placement: a pod counts as blocked when it carries
PodScheduled=Falsewith reasonUnschedulable. Before this release the count was inferred from “phase is Pending and no node assigned”, which also counted a pod the scheduler had not looked at yet — every pod looks like that for the moment between creation and the first scheduling attempt. The donor-selection rule itself did not change: it still keys on DRA claim state, so no order is preempted that was not preempted before. What changes is the number reported in preemption events, which is now the count of pods the scheduler actually refused. No metric carries that number. -
Capacity preemption is now observable. The module emits an event on both objects — on the donor whose replica bound was lowered, naming the recipient, and on the recipient, naming the donor — and the donor’s
ScalingHealthycondition message names the recipient as well. Both events are published only when capacity actually moved: a pass the planner approved that released nothing is counted as its own outcome (result="noop") and stays silent, so an event no longer announces a preemption that did not happen. When the hold ends, the donor gets a closing event naming the bound it was restored to and saying whether the restore was full or partial; a hold that cannot be undone, because the child object is gone, gets its own event instead of falling silent. A partial restore keeps theScalingHealthy=Falsecondition with reasonPreempted, since the donor is still held. Two alerts ship for the first time: capacity was taken, and a donor is held at a lowered bound; the new gauge behind the second one isai_inference_preemption_donors_held. A refusal reason outside the declared allowlist now lands in anOtherbucket onai_inference_preemption_failures_totalinstead of being dropped; before this release it was silently discarded while the attempt counter still recorded the refusal, so the two never added up.ai_inference_preemption_attempts_total{result}now carries six values —ok,noop,refused,error,restored,hold-cleared— of which two are new:noopfor a pass that moved nothing, andhold-clearedfor a hold that ended because the child object is gone. Both used to be counted as something else,okanderrorrespectively, so a query on either of those two counts fewer events after the upgrade. The counter also creates every value of a device class at zero as soon as the class is first observed, socount(),absent()andsum by (result)see series for outcomes that have not happened, and a device class the planner named on a pass that preempted nothing still appears. It does NOT letincrease()see the very first preemption on a device class this controller process has never seen before: that series appears and is incremented between two scrapes, and no in-process initialisation can put a zero sample between them. An earlier edition of this note claimed otherwise. Preemption metrics gain adeviceClasslabel. Theresultlabel is not renamed, so a query naming it still selects the right series — but its VALUE set grew, so a query onresult="ok"orresult="error"counts fewer events than before, as the paragraph above spells out. Object names are deliberately not metric labels — identity lives in the events and in the condition, where it costs no metric cardinality. The device-class label is spelleddeviceClass, camelCase, the same as the field of the resource definition. The events are namedCapacityPreempted(donor),CapacityGranted(recipient),CapacityRestored(donor, hold over) andPreemptionHoldCleared(donor, hold ended with no restore); the alerts areD8AIInferenceCapacityPreemptedandD8AIInferenceDonorHeldAtLoweredBound. The runbooks nameCapacityPreempted,CapacityRestoredandPreemptionHoldClearedin their kubectl commands;CapacityGrantedis read withdescribeor in the events feed. -
The HPA observer no longer writes the scaling condition on an order held by preemption. Two writers of one condition meant the reason
Preempted— which the hold alert’s runbook queries by — was replaced withScalingHealthyon exactly the donors where it matters. Visible in two places: a held donor’s condition now keeps the preemption reason and its message, andai_inference_hpa_observation_totalcounts such a pass asskipped. -
The set of events that wakes the preemption loop changed in one half: it now wakes on a change of the BLOCKED verdict —
PodScheduled=Falsewith reasonUnschedulable— where the same half used to read “phase is Pending and no node is assigned”. The verdict counts in BOTH directions: entering the refusal wakes the loop and so does leaving it. What does not wake it through this half is a transition that leaves the verdict unchanged — between two states neither of which is a placement refusal. Waking on a pod phase change is unchanged, and so is waking on pod creation, which is unconditional and always was. Nothing has to be reconfigured, and no first reaction is delayed: what changes is WHICH updates wake the loop through this half. A pod the scheduler has refused now produces one wake-up at the moment the verdict arrives, which the old rule did not (it already considered such a pod blocked from creation); a pod that goes from unscheduled to scheduled without ever being refused produces none, where the old rule woke the loop when the node was assigned. -
History of that release. The delivery bridge pruned settings keys of its child object that it did not want, and the effect was visible on an upgraded cluster. A key an administrator had added with
kubectl edit(an Update, not an Apply) was removed on the next reconcile, because such a key belonged to a manager named after the process and was indistinguishable from a leftover of the module’s own. The bridge deferred theautoscalingkey to another apply manager only while a preemption episode was actually running, and sent it as usual once the episode was over. A key written with a server-side apply under its own field manager was left alone. The bridge itself has since been removed — the platform builds the objects of an order itself — so nothing of this paragraph describes the module today. The advice it gave still holds for the objects the platform builds: keep manual settings under your own field manager (kubectl apply --server-side --field-manager=<your-name>) rather than editing them in place. -
An order whose allocated GPU reports a failed device now triggers a replan. The controller adds the current device class to the exclusion list, asks the planner again, then deletes and rebuilds the workload of the order with the new device — so it restarts on a different card instead of staying on a broken one. Before this release the failure was only acted on when the claim had no allocation, a combination Kubernetes 1.35 and later cannot hold: the API server refuses a device condition for a device that is not allocated in the claim. Orders whose claim never got allocated are unaffected — those were, and still are, caught by the pending grace window. An allocated device that merely reports
Ready=Falsedoes not trigger a replan: that is how a freshly allocated device looks while the driver configures it, and replanning there would move a healthy card off its device class for good. OnlyFailed=Trueon an allocated device is treated as a failure. -
The context-horizon check now also applies on the direct Hugging Face path. An existing order whose
maxModelLenexceeds the window declared by the model’s recipe preset will move its order condition toFalsewith reasonRuntimeParameterNotSupportedat the next reconcile that bumps the generation. Note the asymmetry: only status is patched, so the workload of the order and its pod stay in place — the order reports a failure while the workload keeps running. LowermaxModelLento the model window, or drop the key so the launch strategy default applies. Four models shipped in the recipe presets declare a window today, all in themicrosoftfamily; orders on any other model are unaffected. -
The bundled class now restricts the API endpoint type to
Chat. Before this release the class declared no restriction, so an order for a catalog model whose types did not includeChatwas silently resolved to the first type the catalog offered. Such an order is now refused by name. The class is meant for generative chat, and the declared set is brought in line with the name; for another endpoint type declare your ownInferenceServiceClass. An existingReadyorder keeps running until its next change bumps the generation. Superseded. The bundled class now permits every contract the module supports, and its name no longer states one of them. What that changes for the choice of a class is in the upgrade note at the top of this page. -
On the bundled class, an order that does not state
spec.runtime.endpointTypemay now resolve to a different API endpoint type than before, with no refusal and no warning. Before this release the type was the first entry of the model’s catalogsupportedEndpointTypes; now it is the intersection with[Chat]. For a model advertising[Embeddings, Chat]the resolved type changes fromEmbeddingstoChat, which changesstatus.model.endpointType, the recipe branch and the runtime parameters. Statespec.runtime.endpointTypeexplicitly, or use your own class, if you depend on a specific type. Note the previous behaviour depended on the ordering of types in the catalog rather than on anything in the order. -
An order that states
spec.model.parameterCountbelow the catalog fact will now be sized against the fact. Requested host memory, CPU, model storage and video memory grow accordingly, and a classmaxParameterCountlimit that the understated value slipped past will now refuse the order. -
Breaking for QUERIES over measurements, and silent. Two values of the
resultlabel onai_inference_reconcile_totalchange spelling:application_ensure_transientbecomesworkload_ensure_transient, andapplication_errorbecomesworkload_error. They named the delivery package removed whole in an earlier slice, while the workload stages beside them already named the workload. Nothing in the module reads this counter, so nothing inside it breaks — a query of YOUR own that selects an order by the old spelling will find zero events, not an error, which is the dangerous half: a silent query looks like «nothing happened». Rewrite such a query to the new spelling before upgrading. The series of the old value does not disappear from your measurement store; it stops growing, so a query over a historical window stays good. Nothing is recreated, and no order changes because of this. -
The module is Experimental: expect additive CRD and settings changes between phase slices. Prefer the walkthrough and OpenAPI/CR reference for the current field set before upgrading a live order.
Documentation
- Overview: Overview
- Phase history: Phase walkthrough
- Custom resources: Custom Resources
- User guide: Usage
- Administration guide: Administration
- Examples: Examples
- FAQ: FAQ