Skip to content

Commit 22e2f39

Browse files
authored
Merge pull request #188 from splunk/codex/ray-2.56-disable-biencoder
fix(ai-tier): pin Ray 2.56 and disable BiEncoder
2 parents 2bbd867 + 958f0b5 commit 22e2f39

18 files changed

Lines changed: 387 additions & 75 deletions

config/configs/model-scale.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
applicationScale:
22
AllMinilmL6V2: 1
3-
BiEncoder: 1
3+
BiEncoder: 0
44
CrossEncoder: 1
55
E5LanguageClassifier: 1
66
Entrypoint: 1

docs/deployment/k0s-quick-reference.md

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ standalone via `stage-artifacts`), it additionally needs:
3939

4040
| Resource | Minimum | Why |
4141
|---|---|---|
42-
| Disk (free) | 250 GB | >120 GB for 11 models + buffer for download staging and upload temp files |
42+
| Disk (free) | 250 GB | >120 GB for 10 models + buffer for download staging and upload temp files |
4343
| RAM | 16 GB | Scripts stream large files; less RAM causes swapping and slow uploads |
4444
| CPU | 4 cores | Parallel upload to MinIO/SeaweedFS/S3 |
4545
| Internet | Stable broadband | Downloads >120 GB from HuggingFace; safe to re-run — already-staged models are skipped |
@@ -87,50 +87,60 @@ in the cluster config to those mirrored paths (required for both paths —
8787
air-gap additionally needs every node to resolve that registry with no outbound
8888
internet):
8989

90-
The default Ray and SAIA images currently use the `preview` tag — set `TAG`
91-
to match whatever tag you're mirroring:
90+
The example below mirrors the SAIA, Ray, and SLIM release images with the common
91+
tag `v1.0`, and the Splunk AI Operator with `v2.8`. Set the corresponding image
92+
fields in the cluster config to the mirrored paths.
9293

9394
With `crane` (works on Ubuntu and RHEL 9, no Docker daemon required):
9495

9596
```bash
96-
TAG="preview"
97+
TAG="v1.0"
9798
for repo in \
9899
splunk/ai-tier-saia-data-loader \
99100
splunk/ai-tier-saia-api-v2 \
100101
splunk/ai-tier-saia-api \
101102
splunk/ai-tier-ray-head \
102-
splunk/ai-tier-ray-worker; do
103+
splunk/ai-tier-ray-worker \
104+
splunk/ai-tier-slim-service; do
103105
crane copy "docker.io/${repo}:${TAG}" "<your-registry>/${repo}:${TAG}"
104106
done
107+
108+
crane copy \
109+
"docker.io/kpratyush775/splunk-ai-operator:v2.8" \
110+
"<your-registry>/splunk/splunk-ai-operator:v2.8"
105111
```
106112

107113
With Docker instead:
108114

109115
```bash
110-
TAG="preview"
116+
TAG="v1.0"
111117
for repo in \
112118
splunk/ai-tier-saia-data-loader \
113119
splunk/ai-tier-saia-api-v2 \
114120
splunk/ai-tier-saia-api \
115121
splunk/ai-tier-ray-head \
116-
splunk/ai-tier-ray-worker; do
122+
splunk/ai-tier-ray-worker \
123+
splunk/ai-tier-slim-service; do
117124
docker pull "docker.io/${repo}:${TAG}"
118125
docker tag "docker.io/${repo}:${TAG}" "<your-registry>/${repo}:${TAG}"
119126
docker push "<your-registry>/${repo}:${TAG}"
120127
done
128+
129+
docker pull "docker.io/kpratyush775/splunk-ai-operator:v2.8"
130+
docker tag "docker.io/kpratyush775/splunk-ai-operator:v2.8" \
131+
"<your-registry>/splunk/splunk-ai-operator:v2.8"
132+
docker push "<your-registry>/splunk/splunk-ai-operator:v2.8"
121133
```
122134

123-
After mirroring, replace the five fully qualified `images.ray.*` and
124-
`images.saia.*` values in the cluster config with the corresponding
125-
`<your-registry>/splunk/...:preview` paths. Mirror the Slim and operator images
126-
separately when those components are enabled, using the tags configured for
127-
your release. For the complete air-gap image list and the bulk `crane copy`
128-
alternative, see
135+
After mirroring, replace the fully qualified `images.ray.*`, `images.saia.*`,
136+
`images.slim.apiImage`, and `images.operator.image` values in the cluster
137+
config with the corresponding `<your-registry>/...` paths. For the complete
138+
air-gap image list and the bulk `crane copy` alternative, see
129139
[DEPLOYMENT_GUIDE.md — Phase 2: Mirror Container Images](../../tools/cluster_setup/DEPLOYMENT_GUIDE.md#phase-2--mirror-container-images).
130140

131-
> `preview` is a mutable tag and the workloads use `imagePullPolicy:
132-
> IfNotPresent`. Use a new immutable tag or digest for controlled upgrades;
133-
> rerunning the installer with the same tag may keep the cached image.
141+
> Image tags can be mutable and the workloads use `imagePullPolicy:
142+
> IfNotPresent`. Use an immutable digest for controlled upgrades; rerunning the
143+
> installer with the same tag may keep the cached image.
134144
135145
---
136146

@@ -169,7 +179,7 @@ node count (2 minimum) to get the cluster total.
169179

170180
| Data | Minimum | Notes |
171181
|---|---|---|
172-
| Model weights | 250 GB | >120 GB for 11 models + re-staging headroom |
182+
| Model weights | 250 GB | >120 GB for 10 models + re-staging headroom |
173183
| Runtime data | 100 GB | Grows with usage |
174184
| **Total bucket** | **500 GB+** | Sufficient for now |
175185

@@ -240,7 +250,7 @@ fully automatic, no manual steps needed. Full commands and details:
240250

241251
### Model Setup (Standard Path)
242252

243-
Model weights (>120 GB, 11 models) must land in your object store before the
253+
Model weights (>120 GB, 10 models) must land in your object store before the
244254
AI platform can serve inference.
245255

246256
- **Full (interactive) install** — the installer always prompts whether to
@@ -358,7 +368,7 @@ MinIO/SeaweedFS/S3) — can be the same machine that runs the installer:
358368

359369
| Resource | Minimum | Why |
360370
|---|---|---|
361-
| Disk (free) | 250 GB | >120 GB for 11 models + buffer for download staging and upload temp files |
371+
| Disk (free) | 250 GB | >120 GB for 10 models + buffer for download staging and upload temp files |
362372
| RAM | 16 GB | Scripts stream large files; less RAM causes swapping and slow uploads |
363373
| CPU | 4 cores | Parallel upload to MinIO/SeaweedFS/S3 |
364374
| Internet | Stable broadband | Downloads >120 GB from HuggingFace; safe to re-run — already-staged models are skipped |

internal/controller/aiservice_controller.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,19 @@ func (r *AIServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
202202
return ctrl.Result{}, nil
203203
}
204204

205+
// aiServiceEventFilter is the controller-wide event filter. Owned-resource
206+
// predicates are combined with this filter, so ConfigMap data changes must be
207+
// admitted here as well as by the ConfigMap-specific Owns predicate. This is
208+
// required for values snapshotted into pod templates, such as EMBEDDING_MODEL.
209+
func aiServiceEventFilter() predicate.Predicate {
210+
return predicate.Or(
211+
common.GenerationChangedPredicate(),
212+
common.AnnotationChangedPredicate(),
213+
common.LabelChangedPredicate(),
214+
common.ConfigMapChangedPredicate(),
215+
)
216+
}
217+
205218
// SetupWithManager sets up the controller with the Manager.
206219
func (r *AIServiceReconciler) SetupWithManager(mgr ctrl.Manager) error {
207220
return ctrl.NewControllerManagedBy(mgr).
@@ -226,11 +239,7 @@ func (r *AIServiceReconciler) SetupWithManager(mgr ctrl.Manager) error {
226239
)),
227240
).
228241
// Add predicates to filter events and avoid unnecessary reconciliations
229-
WithEventFilter(predicate.Or(
230-
common.GenerationChangedPredicate(),
231-
common.AnnotationChangedPredicate(),
232-
common.LabelChangedPredicate(),
233-
)).
242+
WithEventFilter(aiServiceEventFilter()).
234243
// Configure concurrency control
235244
WithOptions(controller.Options{
236245
MaxConcurrentReconciles: aiv1.TotalWorker,

internal/controller/aiservice_controller_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package controller
1919
import (
2020
"context"
2121
"os"
22+
"testing"
2223

2324
. "github.com/onsi/ginkgo/v2"
2425
. "github.com/onsi/gomega"
@@ -34,6 +35,7 @@ import (
3435
ctrl "sigs.k8s.io/controller-runtime"
3536
"sigs.k8s.io/controller-runtime/pkg/client"
3637
"sigs.k8s.io/controller-runtime/pkg/client/fake"
38+
"sigs.k8s.io/controller-runtime/pkg/event"
3739
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3840
)
3941

@@ -415,3 +417,39 @@ var _ = Describe("AIService Controller", func() {
415417
})
416418
})
417419
})
420+
421+
func TestAIServiceEventFilter(t *testing.T) {
422+
oldConfig := &corev1.ConfigMap{
423+
ObjectMeta: metav1.ObjectMeta{Name: "test-saia-config", Namespace: "default", ResourceVersion: "1"},
424+
Data: map[string]string{"EMBEDDING_MODEL": "uae_large"},
425+
}
426+
427+
t.Run("allows data-only ConfigMap updates through the global filter", func(t *testing.T) {
428+
newConfig := oldConfig.DeepCopy()
429+
newConfig.ResourceVersion = "2"
430+
newConfig.Data["EMBEDDING_MODEL"] = "custom_encoder"
431+
432+
if !aiServiceEventFilter().Update(event.UpdateEvent{ObjectOld: oldConfig, ObjectNew: newConfig}) {
433+
t.Fatal("data-only ConfigMap update was filtered out")
434+
}
435+
})
436+
437+
t.Run("filters ConfigMap updates that do not change data or watched metadata", func(t *testing.T) {
438+
newConfig := oldConfig.DeepCopy()
439+
newConfig.ResourceVersion = "2"
440+
441+
if aiServiceEventFilter().Update(event.UpdateEvent{ObjectOld: oldConfig, ObjectNew: newConfig}) {
442+
t.Fatal("resourceVersion-only ConfigMap update was admitted")
443+
}
444+
})
445+
446+
t.Run("preserves generation-change reconciliation for AIService resources", func(t *testing.T) {
447+
oldService := &aiv1.AIService{ObjectMeta: metav1.ObjectMeta{Generation: 1}}
448+
newService := oldService.DeepCopy()
449+
newService.Generation = 2
450+
451+
if !aiServiceEventFilter().Update(event.UpdateEvent{ObjectOld: oldService, ObjectNew: newService}) {
452+
t.Fatal("AIService generation update was filtered out")
453+
}
454+
})
455+
}

pkg/ai/features/saia/impl.go

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -969,6 +969,40 @@ func saiaEnvFrom(ai *aiv1.AIService) []corev1.EnvFromSource {
969969
}
970970
}
971971

972+
const (
973+
defaultV2EmbeddingModel = "uae_large"
974+
unsupportedV2EmbeddingModel = "bi_encoder"
975+
)
976+
977+
// resolveV2EmbeddingModel reads the optional user override from the shared
978+
// SAIA ConfigMap and returns the model that the v2 API and worker must use.
979+
//
980+
// EMBEDDING_MODEL is added to the containers' explicit env list rather than
981+
// relying on envFrom. Kubernetes gives explicit env entries precedence over
982+
// envFrom entries with the same name, so a legacy bi_encoder value cannot make
983+
// SAIA v2 call the unsupported AI-tier endpoint. Other configured models are
984+
// preserved. SAIA v1 continues to consume the ConfigMap exactly as before.
985+
func (r *SaiaReconciler) resolveV2EmbeddingModel(ctx context.Context, ai *aiv1.AIService) (string, error) {
986+
cm := &corev1.ConfigMap{}
987+
err := r.Get(ctx, types.NamespacedName{
988+
Name: fmt.Sprintf("%s-saia-config", ai.Name),
989+
Namespace: ai.Namespace,
990+
}, cm)
991+
if apierrors.IsNotFound(err) {
992+
return defaultV2EmbeddingModel, nil
993+
}
994+
if err != nil {
995+
return "", fmt.Errorf("fetching SAIA ConfigMap for v2 embedding model: %w", err)
996+
}
997+
998+
configured := cm.Data["EMBEDDING_MODEL"]
999+
trimmed := strings.TrimSpace(configured)
1000+
if trimmed == "" || strings.EqualFold(trimmed, unsupportedV2EmbeddingModel) {
1001+
return defaultV2EmbeddingModel, nil
1002+
}
1003+
return configured, nil
1004+
}
1005+
9721006
// saiaVolumes returns the standard config volume and mount for SAIA pods.
9731007
func saiaVolumes(ai *aiv1.AIService) ([]corev1.Volume, []corev1.VolumeMount) {
9741008
featureConfigName := fmt.Sprintf("splunk-%s-feature-config", ai.Name)
@@ -1193,6 +1227,11 @@ func (r *SaiaReconciler) reconcileSAIAv2Deployment(
11931227
ctx context.Context,
11941228
ai *aiv1.AIService,
11951229
) error {
1230+
embeddingModel, err := r.resolveV2EmbeddingModel(ctx, ai)
1231+
if err != nil {
1232+
return err
1233+
}
1234+
11961235
volumes, mounts := saiaVolumes(ai)
11971236
ports := []corev1.ContainerPort{
11981237
{Name: "http", ContainerPort: 8000},
@@ -1201,7 +1240,10 @@ func (r *SaiaReconciler) reconcileSAIAv2Deployment(
12011240

12021241
env := buildSAIABaseEnv(ai)
12031242
env = append(env, buildV2ExtraEnv(ai)...)
1204-
env = append(env, corev1.EnvVar{Name: "VAULT_TEMPLATE_DISABLED", Value: "true"})
1243+
env = append(env,
1244+
corev1.EnvVar{Name: "EMBEDDING_MODEL", Value: embeddingModel},
1245+
corev1.EnvVar{Name: "VAULT_TEMPLATE_DISABLED", Value: "true"},
1246+
)
12051247
env, volumes, mounts, ports = buildSAIATLSEnv(ai, env, volumes, mounts, ports)
12061248
sort.Slice(env, func(i, j int) bool { return env[i].Name < env[j].Name })
12071249

@@ -1311,6 +1353,11 @@ func (r *SaiaReconciler) reconcileSAIAv2Worker(
13111353
ctx context.Context,
13121354
ai *aiv1.AIService,
13131355
) error {
1356+
embeddingModel, err := r.resolveV2EmbeddingModel(ctx, ai)
1357+
if err != nil {
1358+
return err
1359+
}
1360+
13141361
volumes, mounts := saiaVolumes(ai)
13151362
ports := []corev1.ContainerPort{
13161363
{Name: "metrics", ContainerPort: 8088},
@@ -1331,6 +1378,7 @@ func (r *SaiaReconciler) reconcileSAIAv2Worker(
13311378
// conflate with the v1 worker APScheduler cron (which uses 600s for weekly
13321379
// jobs); v2 reuses the same env name for a different purpose.
13331380
env = append(env,
1381+
corev1.EnvVar{Name: "EMBEDDING_MODEL", Value: embeddingModel},
13341382
corev1.EnvVar{Name: "RUN_TASKS_DELAY_S", Value: "600"},
13351383
corev1.EnvVar{Name: "VAULT_TEMPLATE_DISABLED", Value: "true"},
13361384
corev1.EnvVar{Name: "WORKER_HEARTBEAT_PATH", Value: "/tmp/ingestion_worker_heartbeat"},

0 commit comments

Comments
 (0)