Skip to content

Commit f8d8b05

Browse files
authored
add bbr configmap reconciler and datastore (#2045)
Signed-off-by: Nir Rozenbaum <[email protected]>
1 parent 4d37fbc commit f8d8b05

2 files changed

Lines changed: 193 additions & 0 deletions

File tree

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controller
18+
19+
import (
20+
"context"
21+
"fmt"
22+
23+
corev1 "k8s.io/api/core/v1"
24+
"k8s.io/apimachinery/pkg/api/errors"
25+
ctrl "sigs.k8s.io/controller-runtime"
26+
"sigs.k8s.io/controller-runtime/pkg/client"
27+
"sigs.k8s.io/controller-runtime/pkg/log"
28+
29+
"sigs.k8s.io/gateway-api-inference-extension/pkg/bbr/datastore"
30+
logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
31+
)
32+
33+
type ConfigMapReconciler struct {
34+
client.Reader
35+
Datastore datastore.Datastore
36+
}
37+
38+
func (c *ConfigMapReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
39+
logger := log.FromContext(ctx).V(logutil.DEFAULT)
40+
ctx = ctrl.LoggerInto(ctx, logger)
41+
42+
logger.Info("Reconciling ConfigMap")
43+
44+
configmap := &corev1.ConfigMap{}
45+
err := c.Get(ctx, req.NamespacedName, configmap)
46+
if err != nil && !errors.IsNotFound(err) {
47+
return ctrl.Result{}, fmt.Errorf("unable to get ConfigMap - %w", err)
48+
}
49+
50+
if errors.IsNotFound(err) || !configmap.DeletionTimestamp.IsZero() {
51+
// ConfigMap object got deleted or is marked for deletion.
52+
c.Datastore.ConfigMapDelete(configmap)
53+
return ctrl.Result{}, nil
54+
}
55+
56+
// otherwise, add or update the entries of the configmap
57+
if err := c.Datastore.ConfigMapUpdateOrAddIfNotExist(configmap); err != nil {
58+
return ctrl.Result{}, fmt.Errorf("failed to add or update ConfigMap - %w", err)
59+
}
60+
61+
return ctrl.Result{}, nil
62+
}
63+
64+
func (c *ConfigMapReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error {
65+
return ctrl.NewControllerManagedBy(mgr).
66+
For(&corev1.ConfigMap{}).
67+
Complete(c)
68+
}

pkg/bbr/datastore/datastore.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package datastore
18+
19+
import (
20+
"fmt"
21+
"strings"
22+
"sync"
23+
24+
corev1 "k8s.io/api/core/v1"
25+
"k8s.io/apimachinery/pkg/types"
26+
"k8s.io/apimachinery/pkg/util/sets"
27+
"sigs.k8s.io/yaml"
28+
)
29+
30+
const (
31+
baseModelKey = "baseModel"
32+
adaptersKey = "adapters"
33+
)
34+
35+
// The bbr datastore stores a mapping between a LoRA adapter and its corresponding base model.
36+
// Each ConfigMap object stores a mapping between base model and a set of LoRA adapters and datastore translates this into an in-memory cache.
37+
// In case the number of LoRA adapters is extremely large, it's possible to use multiple ConfigMap objects, each storing a slice of the mapping.
38+
// Base models are not stored in the cache.
39+
type Datastore interface {
40+
ConfigMapUpdateOrAddIfNotExist(configmap *corev1.ConfigMap) error
41+
ConfigMapDelete(configmap *corev1.ConfigMap)
42+
}
43+
44+
// NewDatastore creates a new bbr data store.
45+
func NewDatastore() Datastore {
46+
return &datastore{
47+
loraAdapterToBaseModel: map[string]string{},
48+
configmapAdapters: map[types.NamespacedName]sets.Set[string]{}, // map from configmap namespaced name to its adapters
49+
lock: sync.RWMutex{},
50+
}
51+
}
52+
53+
type datastore struct {
54+
loraAdapterToBaseModel map[string]string // a mapping between a lora adapter and its corresponding base model
55+
configmapAdapters map[types.NamespacedName]sets.Set[string]
56+
lock sync.RWMutex
57+
}
58+
59+
func (ds *datastore) ConfigMapUpdateOrAddIfNotExist(configmap *corev1.ConfigMap) error {
60+
baseModel, newAdapters, err := ds.parseConfigMap(configmap)
61+
if err != nil {
62+
return fmt.Errorf("failed to parse configmap - %w", err)
63+
}
64+
65+
configmapNamespacedName := types.NamespacedName{Namespace: configmap.GetNamespace(), Name: configmap.GetName()}
66+
existingAdapters := sets.Set[string]{}
67+
ds.lock.Lock()
68+
defer ds.lock.Unlock()
69+
if previousAdapters, found := ds.configmapAdapters[configmapNamespacedName]; found {
70+
existingAdapters = previousAdapters
71+
}
72+
adaptersToRemove := existingAdapters.Difference(newAdapters) // adapters in existingAdapters but not in newAdapters.
73+
// update loraAdapterToBaseModel mapping
74+
for adapterToUpdate := range newAdapters {
75+
ds.loraAdapterToBaseModel[adapterToUpdate] = baseModel
76+
}
77+
for adapterToRemove := range adaptersToRemove {
78+
delete(ds.loraAdapterToBaseModel, adapterToRemove)
79+
}
80+
// update configmap NamespacedName to current set of adapters mapping
81+
ds.configmapAdapters[configmapNamespacedName] = newAdapters
82+
83+
return nil
84+
}
85+
86+
func (ds *datastore) ConfigMapDelete(configmap *corev1.ConfigMap) {
87+
configmapNamespacedName := types.NamespacedName{Namespace: configmap.GetNamespace(), Name: configmap.GetName()}
88+
ds.lock.Lock()
89+
defer ds.lock.Unlock()
90+
// delete adapters from loraAdapterToBaseModel mapping
91+
adapters := ds.configmapAdapters[configmapNamespacedName]
92+
for adapter := range adapters {
93+
delete(ds.loraAdapterToBaseModel, adapter)
94+
}
95+
// delete configmap NamespacedName to current set of adapters mapping
96+
delete(ds.configmapAdapters, configmapNamespacedName)
97+
}
98+
99+
// parseConfigMap returns a tuple consisting (base model, set of adapters, error)
100+
// error is set in case the configmap data section is not in the expected format.
101+
func (ds *datastore) parseConfigMap(configmap *corev1.ConfigMap) (string, sets.Set[string], error) {
102+
// parse base model
103+
baseModel, ok := configmap.Data[baseModelKey]
104+
if !ok || strings.TrimSpace(baseModel) == "" {
105+
return "", nil, fmt.Errorf("missing or empty baseModel in ConfigMap %s/%s", configmap.Namespace, configmap.Name)
106+
}
107+
108+
adapters := sets.Set[string]{}
109+
// parse adapters
110+
if raw, ok := configmap.Data[adaptersKey]; ok && strings.TrimSpace(raw) != "" {
111+
var list []string
112+
if err := yaml.Unmarshal([]byte(raw), &list); err != nil {
113+
return "", nil, fmt.Errorf("failed to parse adapters: %w", err)
114+
}
115+
116+
for _, adapter := range list {
117+
if strings.TrimSpace(adapter) == "" {
118+
continue // skip empty entries
119+
}
120+
adapters.Insert(adapter)
121+
}
122+
}
123+
124+
return baseModel, adapters, nil
125+
}

0 commit comments

Comments
 (0)