mirror of
https://github.com/hanzo-ml/kserve.git
synced 2026-10-30 20:53:49 +00:00
Signed-off-by: Pierangelo Di Pilato <[email protected]> Co-authored-by: Sivanantham <[email protected]>
868 lines
30 KiB
Go
868 lines
30 KiB
Go
/*
|
|
Copyright 2025 The KServe Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package llmisvc_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/utils/ptr"
|
|
gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
|
|
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
|
|
"github.com/kserve/kserve/pkg/apis/serving/v1alpha2"
|
|
"github.com/kserve/kserve/pkg/controller/v1alpha2/llmisvc/fixture"
|
|
)
|
|
|
|
var _ = Describe("LLMInferenceService webhook validation", func() {
|
|
var (
|
|
ns *corev1.Namespace
|
|
nsName string
|
|
gateway *gwapiv1.Gateway
|
|
httpRoute *gwapiv1.HTTPRoute
|
|
)
|
|
|
|
BeforeEach(func(ctx SpecContext) {
|
|
nsName = fmt.Sprintf("test-llmisvc-validation-%d", time.Now().UnixNano())
|
|
|
|
ns = &corev1.Namespace{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: nsName,
|
|
},
|
|
}
|
|
Expect(envTest.Client.Create(ctx, ns)).To(Succeed())
|
|
|
|
gateway = fixture.Gateway("test-gateway",
|
|
fixture.InNamespace[*gwapiv1.Gateway](nsName),
|
|
fixture.WithClassName("test-gateway-class"),
|
|
fixture.WithListener(gwapiv1.HTTPProtocolType),
|
|
)
|
|
Expect(envTest.Client.Create(ctx, gateway)).To(Succeed())
|
|
|
|
httpRoute = fixture.HTTPRoute("test-route",
|
|
fixture.InNamespace[*gwapiv1.HTTPRoute](nsName),
|
|
fixture.WithParentRef(fixture.GatewayRef("test-gateway")),
|
|
fixture.WithPath("/test"),
|
|
)
|
|
Expect(envTest.Client.Create(ctx, httpRoute)).To(Succeed())
|
|
|
|
DeferCleanup(func(ctx context.Context) {
|
|
httpRoute := httpRoute
|
|
gateway := gateway
|
|
ns := ns
|
|
envTest.DeleteAll(ctx, httpRoute, gateway, ns)
|
|
})
|
|
})
|
|
|
|
Context("cross-field constraints validation", func() {
|
|
It("should reject LLMInferenceService with both refs and spec in HTTPRoute", func(ctx SpecContext) {
|
|
// given
|
|
llmSvc := fixture.LLMInferenceService("test-both-refs-and-spec",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
fixture.WithHTTPRouteRefs(fixture.HTTPRouteRef("test-route")),
|
|
fixture.WithHTTPRouteSpec(&fixture.HTTPRoute("test-route",
|
|
fixture.WithHTTPRule(
|
|
fixture.Matches(fixture.PathPrefixMatch("/test")),
|
|
fixture.WithBackendRefs(fixture.ServiceRef("test-service", 80, 1)),
|
|
),
|
|
).Spec),
|
|
)
|
|
|
|
// when
|
|
errValidation := envTest.Create(ctx, llmSvc)
|
|
|
|
// then
|
|
Expect(errValidation).To(HaveOccurred())
|
|
Expect(errValidation.Error()).To(ContainSubstring("unsupported configuration"))
|
|
})
|
|
|
|
It("should reject LLMInferenceService with user-defined routes and managed gateway", func(ctx SpecContext) {
|
|
// given
|
|
llmSvc := fixture.LLMInferenceService("test-refs-with-managed-gateway",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
fixture.WithHTTPRouteRefs(fixture.HTTPRouteRef("test-route")),
|
|
fixture.WithManagedGateway(),
|
|
)
|
|
|
|
// when
|
|
errValidation := envTest.Create(ctx, llmSvc)
|
|
|
|
// then
|
|
Expect(errValidation).To(HaveOccurred())
|
|
Expect(errValidation.Error()).To(ContainSubstring("cannot be used with a managed gateway"))
|
|
})
|
|
|
|
It("should accept LLMInferenceService with custom gateway refs and managed route spec without parentRefs", func(ctx SpecContext) {
|
|
// given - custom gateway refs with a route spec that has no parentRefs is valid
|
|
// because the controller overwrites the HTTPRoute's ParentRefs from gateway.refs during reconciliation
|
|
llmSvc := fixture.LLMInferenceService("test-custom-gw-managed-route",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
fixture.WithGatewayRefs(fixture.LLMGatewayRef("test-gateway", nsName)),
|
|
fixture.WithHTTPRouteSpec(&fixture.HTTPRoute("test-route",
|
|
fixture.WithHTTPRule(
|
|
fixture.Matches(fixture.PathPrefixMatch("/test")),
|
|
fixture.WithBackendRefs(fixture.ServiceRef("custom-backend", 8080, 1)),
|
|
),
|
|
).Spec),
|
|
)
|
|
|
|
// then
|
|
Expect(envTest.Client.Create(ctx, llmSvc)).To(Succeed())
|
|
})
|
|
|
|
It("should accept but warn when managed route spec has consistent parentRefs with gateway refs", func(ctx SpecContext) {
|
|
// given - parentRefs match gateway.refs, so it's redundant but valid
|
|
llmSvc := fixture.LLMInferenceService("test-spec-with-gateway-refs",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
fixture.WithGatewayRefs(fixture.LLMGatewayRef("test-gateway", nsName)),
|
|
fixture.WithHTTPRouteSpec(&fixture.HTTPRoute("test-route",
|
|
fixture.WithParentRef(fixture.GatewayParentRef("test-gateway", nsName)),
|
|
fixture.WithHTTPRule(
|
|
fixture.Matches(fixture.PathPrefixMatch("/test")),
|
|
fixture.WithBackendRefs(fixture.ServiceRef("custom-backend", 8080, 1)),
|
|
fixture.Timeouts("30s", "60s"),
|
|
),
|
|
).Spec),
|
|
)
|
|
|
|
// then - should be accepted (redundant but consistent)
|
|
Expect(envTest.Client.Create(ctx, llmSvc)).To(Succeed())
|
|
})
|
|
|
|
It("should reject LLMInferenceService with conflicting parentRefs and gateway refs", func(ctx SpecContext) {
|
|
// given - parentRefs point to a different gateway than gateway.refs
|
|
llmSvc := fixture.LLMInferenceService("test-conflicting-refs",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
fixture.WithGatewayRefs(fixture.LLMGatewayRef("test-gateway", nsName)),
|
|
fixture.WithHTTPRouteSpec(&fixture.HTTPRoute("test-route",
|
|
fixture.WithParentRef(fixture.GatewayParentRef("different-gateway", nsName)),
|
|
fixture.WithHTTPRule(
|
|
fixture.Matches(fixture.PathPrefixMatch("/test")),
|
|
fixture.WithBackendRefs(fixture.ServiceRef("custom-backend", 8080, 1)),
|
|
),
|
|
).Spec),
|
|
)
|
|
|
|
// when
|
|
errValidation := envTest.Create(ctx, llmSvc)
|
|
|
|
// then
|
|
Expect(errValidation).To(HaveOccurred())
|
|
Expect(errValidation.Error()).To(ContainSubstring("conflict"))
|
|
})
|
|
})
|
|
|
|
Context("parallelism constraints validation", func() {
|
|
It("should reject LLMInferenceService with both pipeline and data parallelism", func(ctx SpecContext) {
|
|
// given
|
|
llmSvc := fixture.LLMInferenceService("test-both-pipeline-and-data",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
fixture.WithParallelism(fixture.ParallelismSpec(
|
|
fixture.WithPipelineParallelism(2),
|
|
fixture.WithDataParallelism(4),
|
|
fixture.WithDataLocalParallelism(2),
|
|
)),
|
|
)
|
|
|
|
// when
|
|
errValidation := envTest.Create(ctx, llmSvc)
|
|
|
|
// then
|
|
Expect(errValidation).To(HaveOccurred())
|
|
Expect(errValidation.Error()).To(ContainSubstring("cannot set both pipeline parallelism and data parallelism"))
|
|
})
|
|
|
|
It("should reject LLMInferenceService with data parallelism but missing dataLocal", func(ctx SpecContext) {
|
|
// given
|
|
llmSvc := fixture.LLMInferenceService("test-data-without-datalocal",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
fixture.WithParallelism(fixture.ParallelismSpec(
|
|
fixture.WithDataParallelism(4),
|
|
)),
|
|
)
|
|
|
|
// when
|
|
errValidation := envTest.Create(ctx, llmSvc)
|
|
|
|
// then
|
|
Expect(errValidation).To(HaveOccurred())
|
|
Expect(errValidation.Error()).To(ContainSubstring("dataLocal must be set when data is set"))
|
|
})
|
|
|
|
It("should reject LLMInferenceService with dataLocal parallelism but missing data", func(ctx SpecContext) {
|
|
// given
|
|
llmSvc := fixture.LLMInferenceService("test-datalocal-without-data",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
fixture.WithParallelism(fixture.ParallelismSpec(
|
|
fixture.WithDataLocalParallelism(2),
|
|
)),
|
|
)
|
|
|
|
// when
|
|
errValidation := envTest.Create(ctx, llmSvc)
|
|
|
|
// then
|
|
Expect(errValidation).To(HaveOccurred())
|
|
Expect(errValidation.Error()).To(ContainSubstring("data must be set when dataLocal is set"))
|
|
})
|
|
|
|
It("should reject LLMInferenceService with worker but no parallelism configuration", func(ctx SpecContext) {
|
|
// given
|
|
llmSvc := fixture.LLMInferenceService("test-worker-no-parallelism",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
fixture.WithWorker(fixture.SimpleWorkerPodSpec()),
|
|
)
|
|
|
|
// when
|
|
errValidation := envTest.Create(ctx, llmSvc)
|
|
|
|
// then
|
|
Expect(errValidation).To(HaveOccurred())
|
|
Expect(errValidation.Error()).To(ContainSubstring("when worker is specified, parallelism must be configured"))
|
|
})
|
|
|
|
It("should reject LLMInferenceService with prefill having both pipeline and data parallelism", func(ctx SpecContext) {
|
|
// given
|
|
llmSvc := fixture.LLMInferenceService("test-prefill-both-parallelism",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
fixture.WithPrefillParallelism(fixture.ParallelismSpec(
|
|
fixture.WithPipelineParallelism(2),
|
|
fixture.WithDataParallelism(4),
|
|
fixture.WithDataLocalParallelism(2),
|
|
)),
|
|
)
|
|
|
|
// when
|
|
errValidation := envTest.Create(ctx, llmSvc)
|
|
|
|
// then
|
|
Expect(errValidation).To(HaveOccurred())
|
|
Expect(errValidation.Error()).To(ContainSubstring("cannot set both pipeline parallelism and data parallelism"))
|
|
})
|
|
|
|
It("should reject LLMInferenceService with prefill worker but no parallelism configuration", func(ctx SpecContext) {
|
|
// given
|
|
llmSvc := fixture.LLMInferenceService("test-prefill-worker-no-parallelism",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
fixture.WithPrefillWorker(fixture.SimpleWorkerPodSpec()),
|
|
)
|
|
|
|
// when
|
|
errValidation := envTest.Create(ctx, llmSvc)
|
|
|
|
// then
|
|
Expect(errValidation).To(HaveOccurred())
|
|
Expect(errValidation.Error()).To(ContainSubstring("when worker is specified, parallelism must be configured"))
|
|
})
|
|
|
|
It("should accept LLMInferenceService with valid pipeline parallelism", func(ctx SpecContext) {
|
|
// given
|
|
llmSvc := fixture.LLMInferenceService("test-valid-pipeline",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
fixture.WithParallelism(fixture.ParallelismSpec(
|
|
fixture.WithPipelineParallelism(2),
|
|
)),
|
|
fixture.WithWorker(fixture.SimpleWorkerPodSpec()),
|
|
)
|
|
|
|
// then
|
|
Expect(envTest.Client.Create(ctx, llmSvc)).To(Succeed())
|
|
})
|
|
|
|
It("should accept LLMInferenceService with valid data parallelism", func(ctx SpecContext) {
|
|
// given
|
|
llmSvc := fixture.LLMInferenceService("test-valid-data",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
fixture.WithParallelism(fixture.ParallelismSpec(
|
|
fixture.WithDataParallelism(4),
|
|
fixture.WithDataLocalParallelism(2),
|
|
)),
|
|
fixture.WithWorker(fixture.SimpleWorkerPodSpec()),
|
|
)
|
|
|
|
// then
|
|
Expect(envTest.Client.Create(ctx, llmSvc)).To(Succeed())
|
|
})
|
|
|
|
It("should accept LLMInferenceService with valid prefill parallelism configuration", func(ctx SpecContext) {
|
|
// given
|
|
llmSvc := fixture.LLMInferenceService("test-valid-prefill",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
fixture.WithPrefillParallelism(fixture.ParallelismSpec(
|
|
fixture.WithPipelineParallelism(2),
|
|
)),
|
|
fixture.WithPrefillWorker(fixture.SimpleWorkerPodSpec()),
|
|
)
|
|
|
|
// then
|
|
Expect(envTest.Client.Create(ctx, llmSvc)).To(Succeed())
|
|
})
|
|
|
|
It("should reject LLMInferenceService update with different decode parallelism 'size'", func(ctx SpecContext) {
|
|
name := "test-update-decode-parallelism-different-size"
|
|
// given
|
|
llmSvc := fixture.LLMInferenceService(name,
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
fixture.WithParallelism(fixture.ParallelismSpec(
|
|
fixture.WithDataParallelism(1),
|
|
fixture.WithDataLocalParallelism(8),
|
|
)),
|
|
fixture.WithWorker(fixture.SimpleWorkerPodSpec()),
|
|
)
|
|
|
|
// Consistency check
|
|
Expect(llmSvc.Spec.Parallelism.GetSize()).To(Equal(ptr.To(int32(1))))
|
|
Expect(envTest.Client.Create(ctx, llmSvc)).To(Succeed())
|
|
|
|
updated := &v1alpha2.LLMInferenceService{}
|
|
Expect(envTest.Client.Get(ctx, types.NamespacedName{Namespace: llmSvc.GetNamespace(), Name: llmSvc.GetName()}, updated)).To(Succeed())
|
|
|
|
updated.Spec.Parallelism.Data = ptr.To[int32](8)
|
|
updated.Spec.Parallelism.DataLocal = ptr.To[int32](1)
|
|
|
|
// Consistency check
|
|
Expect(updated.Spec.Parallelism.GetSize()).To(Equal(ptr.To(int32(8))))
|
|
|
|
// then
|
|
Expect(envTest.Client.Update(ctx, updated)).To(HaveOccurred())
|
|
})
|
|
|
|
It("should reject LLMInferenceService update with different prefill parallelism 'size'", func(ctx SpecContext) {
|
|
name := "test-update-prefill-parallelism-different-size"
|
|
// given
|
|
llmSvc := fixture.LLMInferenceService(name,
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
fixture.WithPrefillParallelism(fixture.ParallelismSpec(
|
|
fixture.WithDataParallelism(1),
|
|
fixture.WithDataLocalParallelism(8),
|
|
)),
|
|
fixture.WithPrefillWorker(fixture.SimpleWorkerPodSpec()),
|
|
)
|
|
|
|
// Consistency check
|
|
Expect(llmSvc.Spec.Prefill.Parallelism.GetSize()).To(Equal(ptr.To(int32(1))))
|
|
Expect(envTest.Client.Create(ctx, llmSvc)).To(Succeed())
|
|
|
|
updated := &v1alpha2.LLMInferenceService{}
|
|
Expect(envTest.Client.Get(ctx, types.NamespacedName{Namespace: llmSvc.GetNamespace(), Name: llmSvc.GetName()}, updated)).To(Succeed())
|
|
|
|
updated.Spec.Prefill.Parallelism.Data = ptr.To[int32](9)
|
|
updated.Spec.Prefill.Parallelism.DataLocal = ptr.To[int32](1)
|
|
|
|
// Consistency check
|
|
Expect(updated.Spec.Prefill.Parallelism.GetSize()).To(Equal(ptr.To[int32](9)))
|
|
|
|
// then
|
|
Expect(envTest.Client.Update(ctx, updated)).To(HaveOccurred())
|
|
})
|
|
|
|
It("should accept LLMInferenceService without parallelism configuration", func(ctx SpecContext) {
|
|
// given
|
|
llmSvc := fixture.LLMInferenceService("test-no-parallelism",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
)
|
|
|
|
// then
|
|
Expect(envTest.Client.Create(ctx, llmSvc)).To(Succeed())
|
|
})
|
|
})
|
|
})
|
|
|
|
var _ = Describe("LLMInferenceService API validation", func() {
|
|
var (
|
|
ns *corev1.Namespace
|
|
nsName string
|
|
)
|
|
BeforeEach(func(ctx SpecContext) {
|
|
nsName = fmt.Sprintf("test-llmisvc-api-validation-%d", time.Now().UnixNano())
|
|
|
|
ns = &corev1.Namespace{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: nsName,
|
|
},
|
|
}
|
|
Expect(envTest.Client.Create(ctx, ns)).To(Succeed())
|
|
|
|
DeferCleanup(func(ctx context.Context) {
|
|
ns := ns
|
|
envTest.DeleteAll(ctx, ns)
|
|
})
|
|
})
|
|
Context("scheduler config validation", func() {
|
|
It("should reject LLMInferenceService with scheduler config having neither inline nor ref", func(ctx SpecContext) {
|
|
// given
|
|
llmSvc := fixture.LLMInferenceService("test-scheduler-config-neither",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
)
|
|
// Manually set scheduler config with neither inline nor ref
|
|
llmSvc.Spec.Router = &v1alpha2.RouterSpec{
|
|
Scheduler: &v1alpha2.SchedulerSpec{
|
|
Config: &v1alpha2.SchedulerConfigSpec{},
|
|
},
|
|
}
|
|
|
|
// when
|
|
errValidation := envTest.Create(ctx, llmSvc)
|
|
|
|
// then
|
|
Expect(errValidation).To(HaveOccurred())
|
|
Expect(errValidation.Error()).To(ContainSubstring("either inline or ref is required"))
|
|
})
|
|
|
|
It("should reject LLMInferenceService with scheduler config having both inline and ref", func(ctx SpecContext) {
|
|
// given
|
|
llmSvc := fixture.LLMInferenceService("test-scheduler-config-both",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
)
|
|
// Manually set scheduler config with both inline and ref
|
|
inlineConfig := []byte(`{"key": "value"}`)
|
|
llmSvc.Spec.Router = &v1alpha2.RouterSpec{
|
|
Scheduler: &v1alpha2.SchedulerSpec{
|
|
Config: &v1alpha2.SchedulerConfigSpec{
|
|
Inline: &runtime.RawExtension{Raw: inlineConfig},
|
|
Ref: &corev1.ConfigMapKeySelector{
|
|
LocalObjectReference: corev1.LocalObjectReference{Name: "test-config"},
|
|
Key: "config.yaml",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// when
|
|
errValidation := envTest.Create(ctx, llmSvc)
|
|
|
|
// then
|
|
Expect(errValidation).To(HaveOccurred())
|
|
Expect(errValidation.Error()).To(ContainSubstring("both inline and ref are set"))
|
|
})
|
|
|
|
It("should reject LLMInferenceService with empty inline scheduler config", func(ctx SpecContext) {
|
|
// given
|
|
llmSvc := fixture.LLMInferenceService("test-scheduler-config-empty-inline",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
)
|
|
// Manually set scheduler config with empty inline (use valid but empty JSON object)
|
|
llmSvc.Spec.Router = &v1alpha2.RouterSpec{
|
|
Scheduler: &v1alpha2.SchedulerSpec{
|
|
Config: &v1alpha2.SchedulerConfigSpec{
|
|
Inline: &runtime.RawExtension{Raw: []byte(`{}`)},
|
|
},
|
|
},
|
|
}
|
|
|
|
// when
|
|
errValidation := envTest.Create(ctx, llmSvc)
|
|
|
|
// then
|
|
Expect(errValidation).To(HaveOccurred())
|
|
Expect(errValidation.Error()).To(ContainSubstring("inline configuration is invalid"))
|
|
})
|
|
|
|
It("should reject LLMInferenceService with ref scheduler config having empty name", func(ctx SpecContext) {
|
|
// given
|
|
llmSvc := fixture.LLMInferenceService("test-scheduler-config-empty-ref-name",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
)
|
|
// Manually set scheduler config with ref having empty name
|
|
llmSvc.Spec.Router = &v1alpha2.RouterSpec{
|
|
Scheduler: &v1alpha2.SchedulerSpec{
|
|
Config: &v1alpha2.SchedulerConfigSpec{
|
|
Ref: &corev1.ConfigMapKeySelector{
|
|
LocalObjectReference: corev1.LocalObjectReference{Name: ""},
|
|
Key: "config.yaml",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// when
|
|
errValidation := envTest.Create(ctx, llmSvc)
|
|
|
|
// then
|
|
Expect(errValidation).To(HaveOccurred())
|
|
Expect(errValidation.Error()).To(ContainSubstring("name is empty"))
|
|
})
|
|
|
|
It("should accept LLMInferenceService with valid inline scheduler config", func(ctx SpecContext) {
|
|
// given
|
|
inlineConfig := []byte(`{"schedulingStrategy": "round-robin"}`)
|
|
llmSvc := fixture.LLMInferenceService("test-scheduler-config-valid-inline",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
)
|
|
// Manually set scheduler config with valid inline
|
|
llmSvc.Spec.Router = &v1alpha2.RouterSpec{
|
|
Scheduler: &v1alpha2.SchedulerSpec{
|
|
Config: &v1alpha2.SchedulerConfigSpec{
|
|
Inline: &runtime.RawExtension{Raw: inlineConfig},
|
|
},
|
|
},
|
|
}
|
|
|
|
// then
|
|
Expect(envTest.Client.Create(ctx, llmSvc)).To(Succeed())
|
|
})
|
|
|
|
It("should accept LLMInferenceService with valid ref scheduler config", func(ctx SpecContext) {
|
|
// given
|
|
llmSvc := fixture.LLMInferenceService("test-scheduler-config-valid-ref",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
)
|
|
// Manually set scheduler config with valid ref
|
|
llmSvc.Spec.Router = &v1alpha2.RouterSpec{
|
|
Scheduler: &v1alpha2.SchedulerSpec{
|
|
Config: &v1alpha2.SchedulerConfigSpec{
|
|
Ref: &corev1.ConfigMapKeySelector{
|
|
LocalObjectReference: corev1.LocalObjectReference{Name: "test-config"},
|
|
Key: "config.yaml",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// then
|
|
Expect(envTest.Client.Create(ctx, llmSvc)).To(Succeed())
|
|
})
|
|
|
|
It("should accept LLMInferenceService without scheduler config", func(ctx SpecContext) {
|
|
// given
|
|
llmSvc := fixture.LLMInferenceService("test-no-scheduler-config",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
)
|
|
|
|
// then
|
|
Expect(envTest.Client.Create(ctx, llmSvc)).To(Succeed())
|
|
})
|
|
})
|
|
|
|
Context("scheduler replicas validation", func() {
|
|
It("should reject LLMInferenceService with zero scheduler replicas", func(ctx SpecContext) {
|
|
// given
|
|
llmSvc := fixture.LLMInferenceService("test-zero-scheduler-replicas",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
fixture.WithSchedulerReplicas(0),
|
|
)
|
|
|
|
// when
|
|
errValidation := envTest.Create(ctx, llmSvc)
|
|
|
|
// then
|
|
Expect(errValidation).To(HaveOccurred())
|
|
Expect(errValidation.Error()).To(ContainSubstring("scheduler replicas must be greater than zero"))
|
|
})
|
|
|
|
It("should reject LLMInferenceService with negative scheduler replicas", func(ctx SpecContext) {
|
|
// given
|
|
llmSvc := fixture.LLMInferenceService("test-negative-scheduler-replicas",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
fixture.WithSchedulerReplicas(-1),
|
|
)
|
|
|
|
// when
|
|
errValidation := envTest.Create(ctx, llmSvc)
|
|
|
|
// then
|
|
Expect(errValidation).To(HaveOccurred())
|
|
Expect(errValidation.Error()).To(ContainSubstring("scheduler replicas must be greater than zero"))
|
|
})
|
|
|
|
It("should accept LLMInferenceService with positive scheduler replicas", func(ctx SpecContext) {
|
|
// given
|
|
llmSvc := fixture.LLMInferenceService("test-positive-scheduler-replicas",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
fixture.WithSchedulerReplicas(2),
|
|
)
|
|
|
|
// then
|
|
Expect(envTest.Client.Create(ctx, llmSvc)).To(Succeed())
|
|
})
|
|
|
|
It("should accept LLMInferenceService with scheduler replicas of 1", func(ctx SpecContext) {
|
|
// given
|
|
llmSvc := fixture.LLMInferenceService("test-one-scheduler-replica",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
fixture.WithSchedulerReplicas(1),
|
|
)
|
|
|
|
// then
|
|
Expect(envTest.Client.Create(ctx, llmSvc)).To(Succeed())
|
|
})
|
|
|
|
It("should accept LLMInferenceService without scheduler replicas specified", func(ctx SpecContext) {
|
|
// given
|
|
llmSvc := fixture.LLMInferenceService("test-no-scheduler-replicas",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
fixture.WithManagedScheduler(),
|
|
)
|
|
|
|
// then
|
|
Expect(envTest.Client.Create(ctx, llmSvc)).To(Succeed())
|
|
})
|
|
})
|
|
|
|
Context("Integer value validation", func() {
|
|
It("should reject LLMInferenceService with negative workload replicas", func(ctx SpecContext) {
|
|
// given
|
|
llmSvc := fixture.LLMInferenceService("test-negative-replicas",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
fixture.WithReplicas(-1),
|
|
)
|
|
|
|
// when
|
|
errValidation := envTest.Create(ctx, llmSvc)
|
|
|
|
// then
|
|
Expect(errValidation).To(HaveOccurred(), "Expected the Create call to fail due to a validation error, but it succeeded")
|
|
Expect(errValidation.Error()).To(ContainSubstring("spec.replicas in body should be greater than or equal to 0"))
|
|
})
|
|
|
|
It("should reject LLMInferenceService with negative tensor parallelism", func(ctx SpecContext) {
|
|
// given
|
|
llmSvc := fixture.LLMInferenceService("test-negative-int-parallelism",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
fixture.WithParallelism(fixture.ParallelismSpec(
|
|
fixture.WithTensorParallelism(-1),
|
|
)),
|
|
)
|
|
|
|
// when
|
|
errValidation := envTest.Create(ctx, llmSvc)
|
|
|
|
// then
|
|
Expect(errValidation).To(HaveOccurred(), "Expected the Create call to fail due to a validation error, but it succeeded")
|
|
Expect(errValidation.Error()).To(ContainSubstring("spec.parallelism.tensor in body should be greater than or equal to 1"))
|
|
})
|
|
|
|
It("should reject LLMInferenceService with negative pipeline parallelism", func(ctx SpecContext) {
|
|
// given
|
|
llmSvc := fixture.LLMInferenceService("test-negative-int-pipeline",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
fixture.WithParallelism(fixture.ParallelismSpec(
|
|
fixture.WithPipelineParallelism(-1),
|
|
)),
|
|
)
|
|
|
|
// when
|
|
errValidation := envTest.Create(ctx, llmSvc)
|
|
|
|
// then
|
|
Expect(errValidation).To(HaveOccurred(), "Expected the Create call to fail due to a validation error, but it succeeded")
|
|
Expect(errValidation.Error()).To(ContainSubstring("spec.parallelism.pipeline in body should be greater than or equal to 1"))
|
|
})
|
|
|
|
It("should reject LLMInferenceService with negative data parallelism", func(ctx SpecContext) {
|
|
// given
|
|
llmSvc := fixture.LLMInferenceService("test-negative-data",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
fixture.WithParallelism(fixture.ParallelismSpec(
|
|
fixture.WithDataParallelism(-1),
|
|
fixture.WithDataLocalParallelism(1),
|
|
)),
|
|
)
|
|
|
|
// when
|
|
errValidation := envTest.Create(ctx, llmSvc)
|
|
|
|
// then
|
|
Expect(errValidation).To(HaveOccurred(), "Expected the Create call to fail due to a validation error, but it succeeded")
|
|
Expect(errValidation.Error()).To(ContainSubstring("spec.parallelism.data in body should be greater than or equal to 1"))
|
|
})
|
|
|
|
It("should reject LLMInferenceService with zero dataLocal parallelism", func(ctx SpecContext) {
|
|
// given
|
|
llmSvc := fixture.LLMInferenceService("test-negative-datalocal",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
fixture.WithParallelism(fixture.ParallelismSpec(
|
|
fixture.WithDataParallelism(4),
|
|
fixture.WithDataLocalParallelism(0),
|
|
)),
|
|
)
|
|
|
|
// when
|
|
errValidation := envTest.Create(ctx, llmSvc)
|
|
|
|
// then
|
|
Expect(errValidation).To(HaveOccurred(), "Expected the Create call to fail due to a validation error, but it succeeded")
|
|
Expect(errValidation.Error()).To(ContainSubstring("spec.parallelism.dataLocal in body should be greater than or equal to 1"))
|
|
})
|
|
|
|
It("should reject LLMInferenceService with zero data parallelism RPC Port", func(ctx SpecContext) {
|
|
// given
|
|
llmSvc := fixture.LLMInferenceService("test-zero-data-rpc-port",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
fixture.WithParallelism(fixture.ParallelismSpec(
|
|
fixture.WithDataRPCPort(0),
|
|
)),
|
|
)
|
|
|
|
// when
|
|
errValidation := envTest.Create(ctx, llmSvc)
|
|
|
|
// then
|
|
Expect(errValidation).To(HaveOccurred(), "Expected the Create call to fail due to a validation error, but it succeeded")
|
|
Expect(errValidation.Error()).To(ContainSubstring("spec.parallelism.dataRPCPort in body should be greater than or equal to 1"))
|
|
})
|
|
|
|
It("should reject LLMInferenceService with too large data parallelism RPC Port", func(ctx SpecContext) {
|
|
// given
|
|
llmSvc := fixture.LLMInferenceService("test-max-data-rpc-port-exceeded",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceService](nsName),
|
|
fixture.WithModelURI("hf://facebook/opt-125m"),
|
|
fixture.WithParallelism(fixture.ParallelismSpec(
|
|
fixture.WithDataRPCPort(99999),
|
|
)),
|
|
)
|
|
|
|
// when
|
|
errValidation := envTest.Create(ctx, llmSvc)
|
|
|
|
// then
|
|
Expect(errValidation).To(HaveOccurred(), "Expected the Create call to fail due to a validation error, but it succeeded")
|
|
Expect(errValidation.Error()).To(ContainSubstring("spec.parallelism.dataRPCPort in body should be less than or equal to 65535"))
|
|
})
|
|
})
|
|
})
|
|
|
|
var _ = Describe("LLMInferenceServiceConfig webhook validation", func() {
|
|
var (
|
|
ns *corev1.Namespace
|
|
nsName string
|
|
)
|
|
|
|
BeforeEach(func(ctx SpecContext) {
|
|
nsName = fmt.Sprintf("test-llmisvcconfig-validation-%d", time.Now().UnixNano())
|
|
|
|
ns = &corev1.Namespace{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: nsName,
|
|
},
|
|
}
|
|
Expect(envTest.Client.Create(ctx, ns)).To(Succeed())
|
|
|
|
DeferCleanup(func(ctx context.Context) {
|
|
ns := ns
|
|
envTest.DeleteAll(ctx, ns)
|
|
})
|
|
})
|
|
|
|
Context("scheduler replicas validation", func() {
|
|
It("should reject LLMInferenceServiceConfig with zero scheduler replicas", func(ctx SpecContext) {
|
|
// given
|
|
config := fixture.LLMInferenceServiceConfig("test-config-zero-scheduler-replicas",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceServiceConfig](nsName),
|
|
fixture.WithConfigSchedulerReplicas(0),
|
|
)
|
|
|
|
// when
|
|
errValidation := envTest.Create(ctx, config)
|
|
|
|
// then
|
|
Expect(errValidation).To(HaveOccurred())
|
|
Expect(errValidation.Error()).To(ContainSubstring("scheduler replicas must be greater than zero"))
|
|
})
|
|
|
|
It("should reject LLMInferenceServiceConfig with negative scheduler replicas", func(ctx SpecContext) {
|
|
// given
|
|
config := fixture.LLMInferenceServiceConfig("test-config-negative-scheduler-replicas",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceServiceConfig](nsName),
|
|
fixture.WithConfigSchedulerReplicas(-1),
|
|
)
|
|
|
|
// when
|
|
errValidation := envTest.Create(ctx, config)
|
|
|
|
// then
|
|
Expect(errValidation).To(HaveOccurred())
|
|
Expect(errValidation.Error()).To(ContainSubstring("scheduler replicas must be greater than zero"))
|
|
})
|
|
|
|
It("should accept LLMInferenceServiceConfig with positive scheduler replicas", func(ctx SpecContext) {
|
|
// given
|
|
config := fixture.LLMInferenceServiceConfig("test-config-positive-scheduler-replicas",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceServiceConfig](nsName),
|
|
fixture.WithConfigSchedulerReplicas(2),
|
|
)
|
|
|
|
// then
|
|
Expect(envTest.Client.Create(ctx, config)).To(Succeed())
|
|
})
|
|
|
|
It("should accept LLMInferenceServiceConfig with scheduler replicas of 1", func(ctx SpecContext) {
|
|
// given
|
|
config := fixture.LLMInferenceServiceConfig("test-config-one-scheduler-replica",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceServiceConfig](nsName),
|
|
fixture.WithConfigSchedulerReplicas(1),
|
|
)
|
|
|
|
// then
|
|
Expect(envTest.Client.Create(ctx, config)).To(Succeed())
|
|
})
|
|
|
|
It("should accept LLMInferenceServiceConfig without scheduler replicas specified", func(ctx SpecContext) {
|
|
// given
|
|
config := fixture.LLMInferenceServiceConfig("test-config-no-scheduler-replicas",
|
|
fixture.InNamespace[*v1alpha2.LLMInferenceServiceConfig](nsName),
|
|
fixture.WithConfigManagedScheduler(),
|
|
)
|
|
|
|
// then
|
|
Expect(envTest.Client.Create(ctx, config)).To(Succeed())
|
|
})
|
|
})
|
|
})
|