Skip to content

Commit 2b9daf6

Browse files
kartikxkaushik-rohit
authored andcommitted
feat: profiling kv cache index implementations (#108)
1 parent 6f62751 commit 2b9daf6

File tree

1 file changed

+178
-0
lines changed
  • tests/profiling/kv_cache_index

1 file changed

+178
-0
lines changed
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/*
2+
Copyright 2025 The llm-d 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 main
18+
19+
import (
20+
"context"
21+
"flag"
22+
"fmt"
23+
"math/rand/v2"
24+
"time"
25+
26+
"github.com/llm-d/llm-d-kv-cache-manager/pkg/kvcache/kvblock"
27+
"k8s.io/apimachinery/pkg/util/sets"
28+
)
29+
30+
const modelName = "bert-base-uncased"
31+
32+
type IndexProfileResult struct {
33+
AddTime time.Duration
34+
LookupTime time.Duration
35+
}
36+
37+
// TODO @kartikx: Use a more realistic workload if possible.
38+
func generateWorkloadKeys(numKeys int) []kvblock.Key {
39+
// Uses time as seed to ensure that different profiling runs get different keys.
40+
randGen := rand.New(rand.NewPCG(42, uint64(time.Now().UnixNano())))
41+
42+
keys := make([]kvblock.Key, numKeys)
43+
44+
for i := range numKeys {
45+
keys[i] = kvblock.Key{
46+
ModelName: modelName,
47+
ChunkHash: randGen.Uint64(),
48+
}
49+
}
50+
51+
return keys
52+
}
53+
54+
func averageIndexProfileResults(durations []IndexProfileResult) IndexProfileResult {
55+
if len(durations) == 0 {
56+
return IndexProfileResult{}
57+
}
58+
59+
var total IndexProfileResult
60+
for _, d := range durations {
61+
total.AddTime += d.AddTime
62+
total.LookupTime += d.LookupTime
63+
}
64+
65+
count := time.Duration(len(durations))
66+
return IndexProfileResult{
67+
AddTime: total.AddTime / count,
68+
LookupTime: total.LookupTime / count,
69+
}
70+
}
71+
72+
func profileInMemoryIndex(numTrials, numKeys int) (IndexProfileResult, error) {
73+
return runProfileTrials(func() *kvblock.IndexConfig {
74+
return kvblock.DefaultIndexConfig()
75+
}, numTrials, numKeys)
76+
}
77+
78+
func profileRedisIndex(numTrials, numKeys int) (IndexProfileResult, error) {
79+
return runProfileTrials(func() *kvblock.IndexConfig {
80+
return &kvblock.IndexConfig{
81+
RedisConfig: kvblock.DefaultRedisIndexConfig(),
82+
EnableMetrics: false,
83+
}
84+
}, numTrials, numKeys)
85+
}
86+
87+
func profileCostIndex(numTrials, numKeys int) (IndexProfileResult, error) {
88+
return runProfileTrials(func() *kvblock.IndexConfig {
89+
return &kvblock.IndexConfig{
90+
CostAwareMemoryConfig: kvblock.DefaultCostAwareMemoryIndexConfig(),
91+
EnableMetrics: false,
92+
}
93+
}, numTrials, numKeys)
94+
}
95+
96+
// runProfileTrials returns averaged results over multiple profiling runs.
97+
func runProfileTrials(createConfig func() *kvblock.IndexConfig, numTrials, numKeys int) (IndexProfileResult, error) {
98+
profileResults := make([]IndexProfileResult, numTrials)
99+
100+
for i := range numTrials {
101+
ctx := context.Background()
102+
103+
indexConfig := createConfig()
104+
index, err := kvblock.NewIndex(ctx, indexConfig)
105+
if err != nil {
106+
return IndexProfileResult{}, fmt.Errorf("failed to create index: %w", err)
107+
}
108+
109+
result, err := measureIndexRun(ctx, index, "pod1", numKeys)
110+
if err != nil {
111+
return IndexProfileResult{}, fmt.Errorf("failed to profile index: %w", err)
112+
}
113+
114+
profileResults[i] = result
115+
}
116+
117+
return averageIndexProfileResults(profileResults), nil
118+
}
119+
120+
// measureIndexRun performs a single profiling measurement of Add and Lookup operations on an index.
121+
func measureIndexRun(ctx context.Context, index kvblock.Index, podName string, numKeys int) (IndexProfileResult, error) {
122+
keys := generateWorkloadKeys(numKeys)
123+
124+
podEntries := []kvblock.PodEntry{{PodIdentifier: podName, DeviceTier: "gpu"}}
125+
podIdentifierSet := sets.Set[string]{}
126+
127+
addStartTime := time.Now()
128+
129+
err := index.Add(ctx, keys, podEntries)
130+
if err != nil {
131+
return IndexProfileResult{}, fmt.Errorf("failed to add entries: %w", err)
132+
}
133+
134+
addTime := time.Since(addStartTime)
135+
136+
lookupStartTime := time.Now()
137+
138+
_, err = index.Lookup(ctx, keys, podIdentifierSet)
139+
if err != nil {
140+
return IndexProfileResult{}, fmt.Errorf("failed to lookup entries: %w", err)
141+
}
142+
143+
lookupTime := time.Since(lookupStartTime)
144+
145+
return IndexProfileResult{
146+
AddTime: addTime,
147+
LookupTime: lookupTime,
148+
}, nil
149+
}
150+
151+
func main() {
152+
var (
153+
numTrials = flag.Int("trials", 5, "Number of profiling trials to run")
154+
numKeys = flag.Int("keys", 100, "Number of keys to use in each profiling run")
155+
)
156+
flag.Parse()
157+
158+
result, err := profileCostIndex(*numTrials, *numKeys)
159+
if err != nil {
160+
fmt.Printf("Failed to profile cost index: %v\n", err)
161+
} else {
162+
fmt.Printf("[Cost Aware] Add: %v Lookup %v \n", result.AddTime, result.LookupTime)
163+
}
164+
165+
result, err = profileRedisIndex(*numTrials, *numKeys)
166+
if err != nil {
167+
fmt.Printf("Failed to profile redis index: %v\n", err)
168+
} else {
169+
fmt.Printf("[Redis] Add: %v Lookup %v \n", result.AddTime, result.LookupTime)
170+
}
171+
172+
result, err = profileInMemoryIndex(*numTrials, *numKeys)
173+
if err != nil {
174+
fmt.Printf("Failed to profile in memory index: %v\n", err)
175+
} else {
176+
fmt.Printf("[InMemory] Add: %v Lookup %v \n", result.AddTime, result.LookupTime)
177+
}
178+
}

0 commit comments

Comments
 (0)