Skip to content
Merged
10 changes: 9 additions & 1 deletion src/lib/inference/serving/adapter-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
getManagedInferencePreparationDescriptor,
getManagedInferenceRecipeRegistrationError,
getManagedInferenceTopologyQualificationDescriptor,
HOST_LOCAL_VLLM_LIFECYCLE_REF,
HOST_LOCAL_VLLM_MATERIALIZER_REF,
listManagedInferenceLifecycleDescriptors,
listManagedInferenceMaterializerDescriptors,
listManagedInferencePreparationDescriptors,
Expand All @@ -19,12 +21,12 @@ import {
SNAPSHOT_COPY_AND_EXACT_TEXT_REPLACEMENT_PREPARATION_REF,
} from "./adapter-registry.js";
import { loadManagedInferenceCatalog } from "./catalog-loader.js";
import type { ManagedInferenceServingRecipe } from "./types.js";
import { fixtureManagedClusterSelection } from "./managed-cluster-fixture.test-support.js";
import {
MANAGED_CLUSTER_TOPOLOGY_ID,
MANAGED_CLUSTER_TOPOLOGY_SCHEMA_VERSION,
} from "./managed-cluster-topology.js";
import type { ManagedInferenceServingRecipe } from "./types.js";

function shippedRecipe(): ManagedInferenceServingRecipe {
const recipe = loadManagedInferenceCatalog().recipes.find(
Expand All @@ -45,9 +47,11 @@ describe("managed inference adapter registries", () => {
]);
expect(listManagedInferenceMaterializerDescriptors()).toMatchObject([
{ ref: MANAGED_CLUSTER_VLLM_MATERIALIZER_REF, backend: "vllm" },
{ ref: HOST_LOCAL_VLLM_MATERIALIZER_REF, backend: "vllm" },
]);
expect(listManagedInferenceLifecycleDescriptors()).toMatchObject([
{ ref: MANAGED_CLUSTER_VLLM_LIFECYCLE_REF, backend: "vllm" },
{ ref: HOST_LOCAL_VLLM_LIFECYCLE_REF, backend: "vllm" },
]);
expect(listManagedInferencePreparationDescriptors()).toEqual(
expect.arrayContaining([
Expand All @@ -73,10 +77,14 @@ describe("managed inference adapter registries", () => {
expect(
getManagedInferenceMaterializerDescriptor(MANAGED_CLUSTER_VLLM_MATERIALIZER_REF),
).toBeDefined();
expect(
getManagedInferenceMaterializerDescriptor(HOST_LOCAL_VLLM_MATERIALIZER_REF),
).toBeDefined();
expect(getManagedInferenceMaterializerDescriptor("unknown.materializer/v1")).toBeUndefined();
expect(
getManagedInferenceLifecycleDescriptor(MANAGED_CLUSTER_VLLM_LIFECYCLE_REF),
).toBeDefined();
expect(getManagedInferenceLifecycleDescriptor(HOST_LOCAL_VLLM_LIFECYCLE_REF)).toBeDefined();
expect(getManagedInferenceLifecycleDescriptor("unknown.lifecycle/v1")).toBeUndefined();
expect(
getManagedInferencePreparationDescriptor(
Expand Down
118 changes: 104 additions & 14 deletions src/lib/inference/serving/adapter-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
MANAGED_CLUSTER_TOPOLOGY_SCHEMA_VERSION,
} from "./managed-cluster-topology.js";
import type {
HostLocalInferenceServingRecipe,
ManagedInferenceRuntimeServingRecipe,
ManagedInferenceServingRecipe,
ManagedInferenceTopologyQualification,
ServingCatalogRegistries,
Expand All @@ -16,6 +18,8 @@ import type {

export const MANAGED_CLUSTER_VLLM_MATERIALIZER_REF = "vllm.managed-cluster/v1" as const;
export const MANAGED_CLUSTER_VLLM_LIFECYCLE_REF = "vllm.managed-cluster.lifecycle/v1" as const;
export const HOST_LOCAL_VLLM_MATERIALIZER_REF = "vllm.host-local/v1" as const;
export const HOST_LOCAL_VLLM_LIFECYCLE_REF = "vllm.host-local.lifecycle/v1" as const;
export const LLAMA_CPP_HOST_LOCAL_RECEIPT_REF = "llama-cpp.host-local.receipt/v1" as const;
export const LLAMA_CPP_HOST_LOCAL_MATERIALIZER_REF = "llama-cpp.host-local/v1" as const;
export const LLAMA_CPP_HOST_LOCAL_LIFECYCLE_REF = "llama-cpp.host-local.lifecycle/v1" as const;
Expand Down Expand Up @@ -44,12 +48,12 @@ export interface ManagedInferenceMaterializerDescriptor {
readonly ref: string;
readonly backend: string;
readonly outputPlanSchema: string;
readonly topology: {
readonly topology?: {
readonly qualificationId: string;
readonly schemaVersion: number;
readonly outputSchema: string;
};
validateRecipe(recipe: ManagedInferenceServingRecipe): string | undefined;
validateRecipe(recipe: ManagedInferenceRuntimeServingRecipe): string | undefined;
}

export interface ManagedInferenceLifecycleDescriptor {
Expand All @@ -58,14 +62,14 @@ export interface ManagedInferenceLifecycleDescriptor {
readonly acceptedMaterializerRefs: readonly string[];
readonly acceptedPlanSchemas: readonly string[];
readonly secretHandlePermissions: readonly string[];
validateRecipe(recipe: ManagedInferenceServingRecipe): string | undefined;
validateRecipe(recipe: ManagedInferenceRuntimeServingRecipe): string | undefined;
}

export interface ManagedInferencePreparationDescriptor {
readonly ref: string;
readonly backend: string;
readonly phase: "container-before-exec";
validateRecipe(recipe: ManagedInferenceServingRecipe): string | undefined;
validateRecipe(recipe: ManagedInferenceRuntimeServingRecipe): string | undefined;
}

const MANAGED_CLUSTER_TOPOLOGY_OUTPUT_SCHEMA =
Expand All @@ -92,14 +96,26 @@ export function isManagedClusterMaterializerOwnedEnvironment(name: string): bool
return MANAGED_CLUSTER_MATERIALIZER_OWNED_ENVIRONMENT.has(name);
}

export function isManagedClusterInferenceServingRecipe(
recipe: ManagedInferenceRuntimeServingRecipe,
): recipe is ManagedInferenceServingRecipe {
return recipe.spec.execution.materializerRef === MANAGED_CLUSTER_VLLM_MATERIALIZER_REF;
}

export function isHostLocalInferenceServingRecipe(
recipe: ManagedInferenceRuntimeServingRecipe,
): recipe is HostLocalInferenceServingRecipe {
return recipe.spec.execution.materializerRef === HOST_LOCAL_VLLM_MATERIALIZER_REF;
}

function managedClusterTopologyBinding(
recipe: ManagedInferenceServingRecipe,
): ManagedInferenceServingRecipe["spec"]["bindings"][string] | undefined {
return recipe.spec.bindings[recipe.spec.execution.topologyBinding];
}

function positiveIntegerArgument(
recipe: ManagedInferenceServingRecipe,
recipe: ManagedInferenceRuntimeServingRecipe,
name: string,
maximum = Number.MAX_SAFE_INTEGER,
): number | undefined {
Expand All @@ -116,10 +132,10 @@ function positiveIntegerArgument(
}

function validateManagedClusterMaterializerRecipe(
recipe: ManagedInferenceServingRecipe,
recipe: ManagedInferenceRuntimeServingRecipe,
): string | undefined {
if (recipe.spec.backend !== "vllm") return "managed cluster materializer requires backend vllm";
if (recipe.spec.execution.materializerRef !== MANAGED_CLUSTER_VLLM_MATERIALIZER_REF) {
if (!isManagedClusterInferenceServingRecipe(recipe)) {
return "recipe does not select the managed cluster materializer";
}
const { execution } = recipe.spec;
Expand Down Expand Up @@ -241,15 +257,63 @@ function validateManagedClusterMaterializerRecipe(
}

function validateManagedClusterLifecycleRecipe(
recipe: ManagedInferenceServingRecipe,
recipe: ManagedInferenceRuntimeServingRecipe,
): string | undefined {
if (!isManagedClusterInferenceServingRecipe(recipe)) {
return "recipe does not select the managed cluster lifecycle";
}
const materializerError = validateManagedClusterMaterializerRecipe(recipe);
if (materializerError) return materializerError;
return recipe.spec.execution.lifecycleRef === MANAGED_CLUSTER_VLLM_LIFECYCLE_REF
? undefined
: "recipe does not select the managed cluster lifecycle";
}

function validateHostLocalVllmMaterializerRecipe(
recipe: HostLocalInferenceServingRecipe,
): string | undefined {
if (recipe.spec.backend !== "vllm") return "host-local vLLM materializer requires backend vllm";
if (recipe.spec.execution.materializerRef !== HOST_LOCAL_VLLM_MATERIALIZER_REF) {
return "recipe does not select the host-local vLLM materializer";
}
const execution = recipe.spec.execution;
if (
execution.topologyBinding !== undefined ||
execution.nodeCount !== undefined ||
execution.tensorParallelSize !== undefined ||
execution.pipelineParallelSize !== undefined ||
execution.distributedExecutorBackend !== undefined ||
execution.rendezvousPort !== undefined
) {
return "host-local vLLM materializer does not accept distributed execution settings";
}
const bindings = recipe.spec.bindings;
if (bindings !== undefined) {
return "host-local vLLM materializer does not accept topology bindings";
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
const runtime = recipe.spec.runtime;
if (runtime.architecture !== "arm64") {
return "host-local vLLM materializer requires an arm64 runtime";
}
if (recipe.spec.model.preparation?.ref !== NO_PREPARATION_REF) {
return "host-local vLLM materializer currently requires an empty preparation operation";
}
if (recipe.spec.readiness.expectedModel !== recipe.spec.model.servedName) {
return "host-local vLLM readiness must expect the recipe served model";
}
return undefined;
}

function validateHostLocalVllmLifecycleRecipe(
recipe: HostLocalInferenceServingRecipe,
): string | undefined {
const materializerError = validateHostLocalVllmMaterializerRecipe(recipe);
if (materializerError) return materializerError;
return recipe.spec.execution.lifecycleRef === HOST_LOCAL_VLLM_LIFECYCLE_REF
? undefined
: "recipe does not select the host-local vLLM lifecycle";
}

interface SnapshotPreparationInput {
readonly ref: typeof SNAPSHOT_COPY_AND_EXACT_TEXT_REPLACEMENT_PREPARATION_REF;
readonly snapshotCopy: {
Expand All @@ -271,7 +335,7 @@ interface NoPreparationInput {
type ManagedInferencePreparationInput = SnapshotPreparationInput | NoPreparationInput;

function recipePreparation(
recipe: ManagedInferenceServingRecipe,
recipe: ManagedInferenceRuntimeServingRecipe,
): ManagedInferencePreparationInput | undefined {
const preparation = (recipe.spec.model as unknown as { readonly preparation?: unknown })
.preparation;
Expand Down Expand Up @@ -314,7 +378,7 @@ function safeAbsoluteContainerPath(value: unknown): value is string {
}

function validateSnapshotPreparationRecipe(
recipe: ManagedInferenceServingRecipe,
recipe: ManagedInferenceRuntimeServingRecipe,
): string | undefined {
if (recipe.spec.backend !== "vllm") return "snapshot preparation requires backend vllm";
const preparation = recipePreparation(recipe);
Expand Down Expand Up @@ -353,7 +417,9 @@ function validateSnapshotPreparationRecipe(
return undefined;
}

function validateNoPreparationRecipe(recipe: ManagedInferenceServingRecipe): string | undefined {
function validateNoPreparationRecipe(
recipe: ManagedInferenceRuntimeServingRecipe,
): string | undefined {
if (recipe.spec.backend !== "vllm") return "empty preparation requires backend vllm";
const preparation = recipePreparation(recipe);
return preparation?.ref === NO_PREPARATION_REF && hasExactKeys(preparation, ["ref"])
Expand Down Expand Up @@ -383,6 +449,15 @@ const MATERIALIZER_DESCRIPTORS = [
},
validateRecipe: validateManagedClusterMaterializerRecipe,
},
{
ref: HOST_LOCAL_VLLM_MATERIALIZER_REF,
backend: "vllm",
outputPlanSchema: "nemoclaw.nvidia.com/host-local-vllm-plan/v1",
validateRecipe: (recipe) =>
isHostLocalInferenceServingRecipe(recipe)
? validateHostLocalVllmMaterializerRecipe(recipe)
: "recipe does not select the host-local vLLM materializer",
},
] as const satisfies readonly ManagedInferenceMaterializerDescriptor[];

const LIFECYCLE_DESCRIPTORS = [
Expand All @@ -394,6 +469,17 @@ const LIFECYCLE_DESCRIPTORS = [
secretHandlePermissions: ["sshBinding"],
validateRecipe: validateManagedClusterLifecycleRecipe,
},
{
ref: HOST_LOCAL_VLLM_LIFECYCLE_REF,
backend: "vllm",
acceptedMaterializerRefs: [HOST_LOCAL_VLLM_MATERIALIZER_REF],
acceptedPlanSchemas: ["nemoclaw.nvidia.com/host-local-vllm-plan/v1"],
secretHandlePermissions: [],
validateRecipe: (recipe) =>
isHostLocalInferenceServingRecipe(recipe)
? validateHostLocalVllmLifecycleRecipe(recipe)
: "recipe does not select the host-local vLLM lifecycle",
},
] as const satisfies readonly ManagedInferenceLifecycleDescriptor[];

const PREPARATION_DESCRIPTORS = [
Expand Down Expand Up @@ -477,7 +563,7 @@ export function getManagedInferencePreparationDescriptor(
}

export function getManagedInferenceRecipeRegistrationError(
recipe: ManagedInferenceServingRecipe,
recipe: ManagedInferenceRuntimeServingRecipe,
): string | undefined {
const materializer = getManagedInferenceMaterializerDescriptor(
recipe.spec.execution.materializerRef,
Expand Down Expand Up @@ -546,11 +632,15 @@ export function getManagedInferenceServingCatalogRegistries(): ServingCatalogReg
validateRecipe: (recipe: ServingRecipe) => {
if (
recipe.spec.execution.materializerRef !== MANAGED_CLUSTER_VLLM_MATERIALIZER_REF &&
recipe.spec.execution.lifecycleRef !== MANAGED_CLUSTER_VLLM_LIFECYCLE_REF
recipe.spec.execution.lifecycleRef !== MANAGED_CLUSTER_VLLM_LIFECYCLE_REF &&
recipe.spec.execution.materializerRef !== HOST_LOCAL_VLLM_MATERIALIZER_REF &&
recipe.spec.execution.lifecycleRef !== HOST_LOCAL_VLLM_LIFECYCLE_REF
) {
return undefined;
}
return getManagedInferenceRecipeRegistrationError(recipe as ManagedInferenceServingRecipe);
return getManagedInferenceRecipeRegistrationError(
recipe as ManagedInferenceRuntimeServingRecipe,
);
},
};
}
52 changes: 52 additions & 0 deletions src/lib/inference/serving/catalog-loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

import { describe, expect, it } from "vitest";
import {
HOST_LOCAL_VLLM_LIFECYCLE_REF,
HOST_LOCAL_VLLM_MATERIALIZER_REF,
MANAGED_CLUSTER_VLLM_LIFECYCLE_REF,
MANAGED_CLUSTER_VLLM_MATERIALIZER_REF,
} from "./adapter-registry";
Expand Down Expand Up @@ -130,6 +132,56 @@ describe("managed inference catalog loader", () => {
expect(catalogDigest).toBe(servingCatalogDigest(catalogContents));
});

it("retains registered host-local vLLM definitions (#8246)", () => {
const servingCatalog = loadServingCatalog();
const sourceRecipe = servingCatalog.recipes.find(({ spec }) => spec.backend === "vllm")!;
expect(sourceRecipe).toBeDefined();
const sourceSpec = sourceRecipe.spec as Exclude<
ServingRecipe["spec"],
{ backend: "install-llama-cpp" }
>;
const sourcePreset = servingCatalog.presets.find(
({ spec }) => spec.plan.recipeRef === sourceRecipe.metadata.id,
)!;
const { bindings: _bindings, ...hostLocalSpec } = sourceSpec;
const execution = {
materializerRef: HOST_LOCAL_VLLM_MATERIALIZER_REF,
lifecycleRef: HOST_LOCAL_VLLM_LIFECYCLE_REF,
};
const recipe = {
...sourceRecipe,
metadata: { id: "test.vllm-host-local-recipe" },
spec: {
...hostLocalSpec,
backend: "vllm",
model: { ...sourceSpec.model, preparation: { ref: "none/v1" } },
execution,
},
} satisfies ServingRecipe;
const preset = {
...sourcePreset,
metadata: { id: "test.vllm-host-local-preset" },
spec: {
...sourcePreset.spec,
selection: "explicit-only",
requirements: {
all: sourcePreset.spec.requirements!.all.filter(
(requirement) => "readiness" in requirement,
),
},
plan: { backend: "vllm", recipeRef: recipe.metadata.id },
},
} as ServingPreset;
const projected = managedInferenceCatalogFromServingCatalog({
...EMPTY_CATALOG,
recipes: [recipe],
presets: [preset],
});

expect(projected.recipes.map(({ metadata }) => metadata.id)).toEqual([recipe.metadata.id]);
expect(projected.presets.map(({ metadata }) => metadata.id)).toEqual([preset.metadata.id]);
});

it("retains managed vLLM definitions while projecting the mixed serving catalog (#8173)", () => {
const servingCatalog = loadServingCatalog();

Expand Down
Loading
Loading