feat: use message LCP cache affinity keys
This commit is contained in:
@@ -63,12 +63,14 @@ func (s *Server) prepareTaskRequest(ctx context.Context, r *http.Request, user *
|
|||||||
return preparedTaskRequest{}, err
|
return preparedTaskRequest{}, err
|
||||||
}
|
}
|
||||||
inputs := make([]store.ConversationMessageInput, 0, len(messages))
|
inputs := make([]store.ConversationMessageInput, 0, len(messages))
|
||||||
|
messageHashes := make([]any, 0, len(messages))
|
||||||
for _, rawMessage := range messages {
|
for _, rawMessage := range messages {
|
||||||
message, _ := rawMessage.(map[string]any)
|
message, _ := rawMessage.(map[string]any)
|
||||||
if message == nil {
|
if message == nil {
|
||||||
message = map[string]any{"content": rawMessage}
|
message = map[string]any{"content": rawMessage}
|
||||||
}
|
}
|
||||||
hash, assetHashes := canonicalConversationMessageHash(message)
|
hash, assetHashes := canonicalConversationMessageHash(message)
|
||||||
|
messageHashes = append(messageHashes, hash)
|
||||||
inputs = append(inputs, store.ConversationMessageInput{
|
inputs = append(inputs, store.ConversationMessageInput{
|
||||||
Hash: hash,
|
Hash: hash,
|
||||||
Role: stringFromRequestAny(message["role"]),
|
Role: stringFromRequestAny(message["role"]),
|
||||||
@@ -83,6 +85,7 @@ func (s *Server) prepareTaskRequest(ctx context.Context, r *http.Request, user *
|
|||||||
preparedBody["conversationId"] = conversationKey
|
preparedBody["conversationId"] = conversationKey
|
||||||
preparedBody["conversationRecordId"] = conversationID
|
preparedBody["conversationRecordId"] = conversationID
|
||||||
preparedBody["messageRefs"] = messageRefsForRequest(refs)
|
preparedBody["messageRefs"] = messageRefsForRequest(refs)
|
||||||
|
preparedBody["messageHashes"] = messageHashes
|
||||||
preparedBody["newMessageCount"] = newCount
|
preparedBody["newMessageCount"] = newCount
|
||||||
delete(preparedBody, "messages")
|
delete(preparedBody, "messages")
|
||||||
result.ConversationID = conversationID
|
result.ConversationID = conversationID
|
||||||
|
|||||||
@@ -7,47 +7,78 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type cacheAffinityKeys struct {
|
||||||
|
Primary string
|
||||||
|
Lookup []string
|
||||||
|
Record []string
|
||||||
|
}
|
||||||
|
|
||||||
func buildCacheAffinityKey(kind string, modelType string, body map[string]any) string {
|
func buildCacheAffinityKey(kind string, modelType string, body map[string]any) string {
|
||||||
|
return buildCacheAffinityKeys(kind, modelType, body).Primary
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildCacheAffinityKeys(kind string, modelType string, body map[string]any) cacheAffinityKeys {
|
||||||
if len(body) == 0 {
|
if len(body) == 0 {
|
||||||
return ""
|
return cacheAffinityKeys{}
|
||||||
}
|
}
|
||||||
for _, key := range []string{"cacheAffinityKey", "cache_affinity_key"} {
|
for _, key := range []string{"cacheAffinityKey", "cache_affinity_key"} {
|
||||||
if value := strings.TrimSpace(stringFromAny(body[key])); value != "" {
|
if value := strings.TrimSpace(stringFromAny(body[key])); value != "" {
|
||||||
return "explicit:" + sha256Text(value)
|
key := "explicit:" + sha256Text(value)
|
||||||
|
return cacheAffinityKeys{Primary: key, Lookup: []string{key}, Record: []string{key}}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, key := range []string{"sessionId", "session_id", "conversationId", "conversation_id"} {
|
for _, key := range []string{"sessionId", "session_id", "conversationId", "conversation_id"} {
|
||||||
if value := strings.TrimSpace(stringFromAny(body[key])); value != "" {
|
if value := strings.TrimSpace(stringFromAny(body[key])); value != "" {
|
||||||
return key + ":" + sha256Text(value)
|
key := key + ":" + sha256Text(value)
|
||||||
|
return cacheAffinityKeys{Primary: key, Lookup: []string{key}, Record: []string{key}}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !cacheAffinityPromptHashSupported(kind, modelType) {
|
if !cacheAffinityPromptHashSupported(kind, modelType) {
|
||||||
return ""
|
return cacheAffinityKeys{}
|
||||||
}
|
}
|
||||||
payload := map[string]any{
|
basePayload := map[string]any{
|
||||||
"kind": kind,
|
"kind": kind,
|
||||||
"modelType": modelType,
|
"modelType": modelType,
|
||||||
}
|
}
|
||||||
if messages, ok := body["messages"]; ok {
|
|
||||||
payload["messages"] = messages
|
|
||||||
}
|
|
||||||
if tools, ok := body["tools"]; ok {
|
if tools, ok := body["tools"]; ok {
|
||||||
payload["tools"] = tools
|
basePayload["tools"] = tools
|
||||||
}
|
}
|
||||||
if instructions, ok := body["instructions"]; ok {
|
if instructions, ok := body["instructions"]; ok {
|
||||||
payload["instructions"] = instructions
|
basePayload["instructions"] = instructions
|
||||||
|
}
|
||||||
|
messageHashes := cacheAffinityMessageHashes(body)
|
||||||
|
if len(messageHashes) > 0 {
|
||||||
|
baseHash := stableJSONHash(basePayload)
|
||||||
|
keys := make([]string, 0, len(messageHashes))
|
||||||
|
for index := range messageHashes {
|
||||||
|
keyPayload := map[string]any{
|
||||||
|
"base": baseHash,
|
||||||
|
"messageHashes": messageHashes[:index+1],
|
||||||
|
"prefixLength": index + 1,
|
||||||
|
}
|
||||||
|
keys = append(keys, "prompt_lcp:"+stableJSONHash(keyPayload))
|
||||||
|
}
|
||||||
|
if len(keys) == 0 {
|
||||||
|
return cacheAffinityKeys{}
|
||||||
|
}
|
||||||
|
lookup := make([]string, 0, len(keys))
|
||||||
|
for index := len(keys) - 1; index >= 0; index-- {
|
||||||
|
lookup = append(lookup, keys[index])
|
||||||
|
}
|
||||||
|
return cacheAffinityKeys{
|
||||||
|
Primary: keys[len(keys)-1],
|
||||||
|
Lookup: lookup,
|
||||||
|
Record: keys,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if input, ok := body["input"]; ok {
|
if input, ok := body["input"]; ok {
|
||||||
payload["input"] = input
|
basePayload["input"] = input
|
||||||
}
|
}
|
||||||
if len(payload) <= 2 {
|
if len(basePayload) <= 2 {
|
||||||
return ""
|
return cacheAffinityKeys{}
|
||||||
}
|
}
|
||||||
raw, err := json.Marshal(payload)
|
key := "prompt:" + stableJSONHash(basePayload)
|
||||||
if err != nil || len(raw) == 0 {
|
return cacheAffinityKeys{Primary: key, Lookup: []string{key}, Record: []string{key}}
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return "prompt:" + sha256Text(string(raw))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func cacheAffinityPromptHashSupported(kind string, modelType string) bool {
|
func cacheAffinityPromptHashSupported(kind string, modelType string) bool {
|
||||||
@@ -63,6 +94,36 @@ func cacheAffinityPromptHashSupported(kind string, modelType string) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cacheAffinityMessageHashes(body map[string]any) []string {
|
||||||
|
if hashes := stringListFromAny(body["messageHashes"]); len(hashes) > 0 {
|
||||||
|
return hashes
|
||||||
|
}
|
||||||
|
if hashes := stringListFromAny(body["message_hashes"]); len(hashes) > 0 {
|
||||||
|
return hashes
|
||||||
|
}
|
||||||
|
messages, ok := body["messages"].([]any)
|
||||||
|
if !ok || len(messages) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
hashes := make([]string, 0, len(messages))
|
||||||
|
for _, message := range messages {
|
||||||
|
raw, err := json.Marshal(message)
|
||||||
|
if err != nil || len(raw) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hashes = append(hashes, sha256Text(string(raw)))
|
||||||
|
}
|
||||||
|
return hashes
|
||||||
|
}
|
||||||
|
|
||||||
|
func stableJSONHash(value any) string {
|
||||||
|
raw, err := json.Marshal(value)
|
||||||
|
if err != nil || len(raw) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return sha256Text(string(raw))
|
||||||
|
}
|
||||||
|
|
||||||
func sha256Text(value string) string {
|
func sha256Text(value string) string {
|
||||||
sum := sha256.Sum256([]byte(value))
|
sum := sha256.Sum256([]byte(value))
|
||||||
return hex.EncodeToString(sum[:])
|
return hex.EncodeToString(sum[:])
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package runner
|
package runner
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func TestBuildCacheAffinityKeyUsesExplicitKeyFirst(t *testing.T) {
|
func TestBuildCacheAffinityKeyUsesExplicitKeyFirst(t *testing.T) {
|
||||||
body := map[string]any{
|
body := map[string]any{
|
||||||
@@ -10,7 +13,7 @@ func TestBuildCacheAffinityKeyUsesExplicitKeyFirst(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
got := buildCacheAffinityKey("chat.completions", "text_generate", body)
|
got := buildCacheAffinityKey("chat.completions", "text_generate", body)
|
||||||
if got == "" || got[:9] != "explicit:" {
|
if !strings.HasPrefix(got, "explicit:") {
|
||||||
t.Fatalf("expected explicit cache affinity key, got %q", got)
|
t.Fatalf("expected explicit cache affinity key, got %q", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,7 +23,7 @@ func TestBuildCacheAffinityKeyUsesSessionContinuity(t *testing.T) {
|
|||||||
"session_id": "conversation-1",
|
"session_id": "conversation-1",
|
||||||
})
|
})
|
||||||
|
|
||||||
if got == "" || got[:11] != "session_id:" {
|
if !strings.HasPrefix(got, "session_id:") {
|
||||||
t.Fatalf("expected session cache affinity key, got %q", got)
|
t.Fatalf("expected session cache affinity key, got %q", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,7 +39,35 @@ func TestBuildCacheAffinityKeyFallsBackToPromptHash(t *testing.T) {
|
|||||||
|
|
||||||
first := buildCacheAffinityKey("chat.completions", "text_generate", body)
|
first := buildCacheAffinityKey("chat.completions", "text_generate", body)
|
||||||
second := buildCacheAffinityKey("chat.completions", "text_generate", body)
|
second := buildCacheAffinityKey("chat.completions", "text_generate", body)
|
||||||
if first == "" || first != second || first[:7] != "prompt:" {
|
if first == "" || first != second || !strings.HasPrefix(first, "prompt_lcp:") {
|
||||||
t.Fatalf("expected stable prompt hash cache affinity key, first=%q second=%q", first, second)
|
t.Fatalf("expected stable prompt LCP cache affinity key, first=%q second=%q", first, second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildCacheAffinityKeysUsesMessagePrefixLCPKeys(t *testing.T) {
|
||||||
|
first := buildCacheAffinityKeys("chat.completions", "text_generate", map[string]any{
|
||||||
|
"messageHashes": []any{"system-hash", "user-hash"},
|
||||||
|
"tools": []any{map[string]any{"type": "function", "function": map[string]any{"name": "lookup"}}},
|
||||||
|
})
|
||||||
|
next := buildCacheAffinityKeys("chat.completions", "text_generate", map[string]any{
|
||||||
|
"messageHashes": []any{"system-hash", "user-hash", "assistant-hash", "tool-hash"},
|
||||||
|
"tools": []any{map[string]any{"type": "function", "function": map[string]any{"name": "lookup"}}},
|
||||||
|
})
|
||||||
|
|
||||||
|
if !strings.HasPrefix(first.Primary, "prompt_lcp:") {
|
||||||
|
t.Fatalf("expected LCP prompt key, got %+v", first)
|
||||||
|
}
|
||||||
|
if len(first.Record) != 2 || len(next.Lookup) != 4 {
|
||||||
|
t.Fatalf("expected per-message prefix keys, first=%+v next=%+v", first, next)
|
||||||
|
}
|
||||||
|
found := false
|
||||||
|
for _, key := range next.Lookup {
|
||||||
|
if key == first.Primary {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("next lookup keys should include first request prefix key, first=%+v next=%+v", first, next)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,9 +149,10 @@ func (s *Service) execute(ctx context.Context, task store.GatewayTask, user *aut
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return Result{}, err
|
return Result{}, err
|
||||||
}
|
}
|
||||||
cacheAffinityKey := buildCacheAffinityKey(task.Kind, modelType, body)
|
cacheAffinityKeys := buildCacheAffinityKeys(task.Kind, modelType, body)
|
||||||
candidates, err := s.store.ListModelCandidates(ctx, task.Model, modelType, user, store.ListModelCandidatesOptions{
|
candidates, err := s.store.ListModelCandidates(ctx, task.Model, modelType, user, store.ListModelCandidatesOptions{
|
||||||
CacheAffinityKey: cacheAffinityKey,
|
CacheAffinityKey: cacheAffinityKeys.Primary,
|
||||||
|
CacheAffinityKeys: cacheAffinityKeys.Lookup,
|
||||||
CacheAffinityPolicy: runnerPolicy.CacheAffinityPolicy,
|
CacheAffinityPolicy: runnerPolicy.CacheAffinityPolicy,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -324,7 +325,7 @@ candidatesLoop:
|
|||||||
break candidatesLoop
|
break candidatesLoop
|
||||||
}
|
}
|
||||||
candidateBody := preprocessing.Body
|
candidateBody := preprocessing.Body
|
||||||
response, err := s.runCandidate(ctx, task, user, candidateBody, preprocessing.Log, candidate, nextAttemptNo, onDelta, singleSourceProtected, runnerPolicy.CacheAffinityPolicy)
|
response, err := s.runCandidate(ctx, task, user, candidateBody, preprocessing.Log, candidate, nextAttemptNo, onDelta, singleSourceProtected, runnerPolicy.CacheAffinityPolicy, cacheAffinityKeys.Record)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
attemptNo = nextAttemptNo
|
attemptNo = nextAttemptNo
|
||||||
billings := s.billings(ctx, user, task.Kind, candidateBody, candidate, response, isSimulation(task, candidate))
|
billings := s.billings(ctx, user, task.Kind, candidateBody, candidate, response, isSimulation(task, candidate))
|
||||||
@@ -535,7 +536,7 @@ candidatesLoop:
|
|||||||
return Result{Task: failed, Output: failed.Result}, lastErr
|
return Result{Task: failed, Output: failed.Result}, lastErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) runCandidate(ctx context.Context, task store.GatewayTask, user *auth.User, body map[string]any, preprocessing parameterPreprocessingLog, candidate store.RuntimeModelCandidate, attemptNo int, onDelta clients.StreamDelta, singleSourceProtected bool, cacheAffinityPolicy map[string]any) (clients.Response, error) {
|
func (s *Service) runCandidate(ctx context.Context, task store.GatewayTask, user *auth.User, body map[string]any, preprocessing parameterPreprocessingLog, candidate store.RuntimeModelCandidate, attemptNo int, onDelta clients.StreamDelta, singleSourceProtected bool, cacheAffinityPolicy map[string]any, cacheAffinityRecordKeys []string) (clients.Response, error) {
|
||||||
simulated := isSimulation(task, candidate)
|
simulated := isSimulation(task, candidate)
|
||||||
baseAttemptMetrics := mergeMetrics(attemptMetrics(candidate, attemptNo, simulated), parameterPreprocessingMetrics(preprocessing))
|
baseAttemptMetrics := mergeMetrics(attemptMetrics(candidate, attemptNo, simulated), parameterPreprocessingMetrics(preprocessing))
|
||||||
reservations := s.rateLimitReservations(ctx, user, candidate, body)
|
reservations := s.rateLimitReservations(ctx, user, candidate, body)
|
||||||
@@ -769,6 +770,7 @@ func (s *Service) runCandidate(ctx context.Context, task store.GatewayTask, user
|
|||||||
}
|
}
|
||||||
if err := s.store.RecordCacheAffinityObservation(context.WithoutCancel(ctx), store.CacheAffinityObservationInput{
|
if err := s.store.RecordCacheAffinityObservation(context.WithoutCancel(ctx), store.CacheAffinityObservationInput{
|
||||||
CacheAffinityKey: candidate.CacheAffinity.Key,
|
CacheAffinityKey: candidate.CacheAffinity.Key,
|
||||||
|
CacheAffinityKeys: cacheAffinityRecordKeys,
|
||||||
CacheAffinityPolicy: cacheAffinityPolicy,
|
CacheAffinityPolicy: cacheAffinityPolicy,
|
||||||
PlatformID: candidate.PlatformID,
|
PlatformID: candidate.PlatformID,
|
||||||
PlatformModelID: candidate.PlatformModelID,
|
PlatformModelID: candidate.PlatformModelID,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
type CacheAffinityObservationInput struct {
|
type CacheAffinityObservationInput struct {
|
||||||
CacheAffinityKey string
|
CacheAffinityKey string
|
||||||
|
CacheAffinityKeys []string
|
||||||
CacheAffinityPolicy map[string]any
|
CacheAffinityPolicy map[string]any
|
||||||
PlatformID string
|
PlatformID string
|
||||||
PlatformModelID string
|
PlatformModelID string
|
||||||
@@ -19,9 +20,10 @@ type CacheAffinityObservationInput struct {
|
|||||||
|
|
||||||
func (s *Store) RecordCacheAffinityObservation(ctx context.Context, input CacheAffinityObservationInput) error {
|
func (s *Store) RecordCacheAffinityObservation(ctx context.Context, input CacheAffinityObservationInput) error {
|
||||||
input.CacheAffinityKey = strings.TrimSpace(input.CacheAffinityKey)
|
input.CacheAffinityKey = strings.TrimSpace(input.CacheAffinityKey)
|
||||||
|
input.CacheAffinityKeys = normalizedCacheAffinityKeys(input.CacheAffinityKey, input.CacheAffinityKeys)
|
||||||
input.ClientID = strings.TrimSpace(input.ClientID)
|
input.ClientID = strings.TrimSpace(input.ClientID)
|
||||||
input.ModelType = strings.TrimSpace(input.ModelType)
|
input.ModelType = strings.TrimSpace(input.ModelType)
|
||||||
if input.CacheAffinityKey == "" || input.ClientID == "" || input.ModelType == "" {
|
if len(input.CacheAffinityKeys) == 0 || input.ClientID == "" || input.ModelType == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if !input.CachedInputTokensKnown || !cacheAffinityPolicyEnabled(input.CacheAffinityPolicy, input.ModelType) {
|
if !input.CachedInputTokensKnown || !cacheAffinityPolicyEnabled(input.CacheAffinityPolicy, input.ModelType) {
|
||||||
@@ -39,6 +41,15 @@ func (s *Store) RecordCacheAffinityObservation(ctx context.Context, input CacheA
|
|||||||
}
|
}
|
||||||
alpha := cacheAffinityEMAAlpha(input.CacheAffinityPolicy)
|
alpha := cacheAffinityEMAAlpha(input.CacheAffinityPolicy)
|
||||||
hitRatio := float64(input.CachedInputTokens) / float64(input.InputTokens)
|
hitRatio := float64(input.CachedInputTokens) / float64(input.InputTokens)
|
||||||
|
for _, key := range input.CacheAffinityKeys {
|
||||||
|
if err := s.recordCacheAffinityObservationKey(ctx, input, key, hitRatio, alpha); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) recordCacheAffinityObservationKey(ctx context.Context, input CacheAffinityObservationInput, key string, hitRatio float64, alpha float64) error {
|
||||||
_, err := s.pool.Exec(ctx, `
|
_, err := s.pool.Exec(ctx, `
|
||||||
INSERT INTO gateway_cache_affinity_stats (
|
INSERT INTO gateway_cache_affinity_stats (
|
||||||
client_id, cache_affinity_key, platform_id, platform_model_id, model_type,
|
client_id, cache_affinity_key, platform_id, platform_model_id, model_type,
|
||||||
@@ -60,7 +71,7 @@ func (s *Store) RecordCacheAffinityObservation(ctx context.Context, input CacheA
|
|||||||
last_observed_at = now(),
|
last_observed_at = now(),
|
||||||
updated_at = now()`,
|
updated_at = now()`,
|
||||||
input.ClientID,
|
input.ClientID,
|
||||||
input.CacheAffinityKey,
|
key,
|
||||||
input.PlatformID,
|
input.PlatformID,
|
||||||
input.PlatformModelID,
|
input.PlatformModelID,
|
||||||
input.ModelType,
|
input.ModelType,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
type ListModelCandidatesOptions struct {
|
type ListModelCandidatesOptions struct {
|
||||||
CacheAffinityKey string
|
CacheAffinityKey string
|
||||||
|
CacheAffinityKeys []string
|
||||||
CacheAffinityPolicy map[string]any
|
CacheAffinityPolicy map[string]any
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@ func (s *Store) ListModelCandidates(ctx context.Context, model string, modelType
|
|||||||
COALESCE(s.waiting_count, 0)::float8,
|
COALESCE(s.waiting_count, 0)::float8,
|
||||||
COALESCE(s.limiter_ratio, 0)::float8,
|
COALESCE(s.limiter_ratio, 0)::float8,
|
||||||
COALESCE(EXTRACT(EPOCH FROM s.last_assigned_at), 0)::float8,
|
COALESCE(EXTRACT(EPOCH FROM s.last_assigned_at), 0)::float8,
|
||||||
|
COALESCE(ca.cache_affinity_key, '')::text,
|
||||||
COALESCE(ca.request_count, 0)::float8,
|
COALESCE(ca.request_count, 0)::float8,
|
||||||
COALESCE(ca.input_tokens, 0)::float8,
|
COALESCE(ca.input_tokens, 0)::float8,
|
||||||
COALESCE(ca.cached_input_tokens, 0)::float8,
|
COALESCE(ca.cached_input_tokens, 0)::float8,
|
||||||
@@ -59,10 +61,15 @@ func (s *Store) ListModelCandidates(ctx context.Context, model string, modelType
|
|||||||
LEFT JOIN model_runtime_policy_sets rp ON rp.id = COALESCE(m.runtime_policy_set_id, b.runtime_policy_set_id)
|
LEFT JOIN model_runtime_policy_sets rp ON rp.id = COALESCE(m.runtime_policy_set_id, b.runtime_policy_set_id)
|
||||||
LEFT JOIN runtime_client_states s
|
LEFT JOIN runtime_client_states s
|
||||||
ON s.client_id = p.platform_key || ':' || $2::text || ':' || COALESCE(NULLIF(m.provider_model_name, ''), m.model_name)
|
ON s.client_id = p.platform_key || ':' || $2::text || ':' || COALESCE(NULLIF(m.provider_model_name, ''), m.model_name)
|
||||||
LEFT JOIN gateway_cache_affinity_stats ca
|
LEFT JOIN LATERAL (
|
||||||
ON ca.client_id = p.platform_key || ':' || $2::text || ':' || COALESCE(NULLIF(m.provider_model_name, ''), m.model_name)
|
SELECT ca.cache_affinity_key, ca.request_count, ca.input_tokens, ca.cached_input_tokens, ca.ema_hit_ratio, ca.last_hit_ratio, ca.last_observed_at
|
||||||
AND ca.cache_affinity_key = NULLIF($4::text, '')
|
FROM unnest($4::text[]) WITH ORDINALITY AS affinity_keys(cache_affinity_key, affinity_rank)
|
||||||
AND ($5::int <= 0 OR ca.last_observed_at >= now() - ($5::int * interval '1 second'))
|
JOIN gateway_cache_affinity_stats ca ON ca.cache_affinity_key = affinity_keys.cache_affinity_key
|
||||||
|
WHERE ca.client_id = p.platform_key || ':' || $2::text || ':' || COALESCE(NULLIF(m.provider_model_name, ''), m.model_name)
|
||||||
|
AND ($5::int <= 0 OR ca.last_observed_at >= now() - ($5::int * interval '1 second'))
|
||||||
|
ORDER BY affinity_keys.affinity_rank ASC, ca.cached_input_tokens DESC, ca.ema_hit_ratio DESC, ca.last_observed_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
) ca ON TRUE
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT scope_key, SUM(lease_value) AS active
|
SELECT scope_key, SUM(lease_value) AS active
|
||||||
FROM gateway_concurrency_leases
|
FROM gateway_concurrency_leases
|
||||||
@@ -175,7 +182,7 @@ WHERE p.status = 'enabled'
|
|||||||
COALESCE(s.running_count, 0) ASC,
|
COALESCE(s.running_count, 0) ASC,
|
||||||
COALESCE(s.waiting_count, 0) ASC,
|
COALESCE(s.waiting_count, 0) ASC,
|
||||||
COALESCE(s.last_assigned_at, to_timestamp(0)) ASC,
|
COALESCE(s.last_assigned_at, to_timestamp(0)) ASC,
|
||||||
m.created_at ASC`, exactModel, modelType, modelMatchKey, listOptions.CacheAffinityKey, cacheAffinityStaleAfterSeconds(listOptions.CacheAffinityPolicy))
|
m.created_at ASC`, exactModel, modelType, modelMatchKey, listOptions.CacheAffinityKeys, cacheAffinityStaleAfterSeconds(listOptions.CacheAffinityPolicy))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -211,6 +218,7 @@ WHERE p.status = 'enabled'
|
|||||||
var stateWaitingCount float64
|
var stateWaitingCount float64
|
||||||
var stateLimiterRatio float64
|
var stateLimiterRatio float64
|
||||||
var lastAssignedUnix float64
|
var lastAssignedUnix float64
|
||||||
|
var cacheAffinityKey string
|
||||||
var cacheRequestCount float64
|
var cacheRequestCount float64
|
||||||
var cacheInputTokens float64
|
var cacheInputTokens float64
|
||||||
var cacheCachedInputTokens float64
|
var cacheCachedInputTokens float64
|
||||||
@@ -269,6 +277,7 @@ WHERE p.status = 'enabled'
|
|||||||
&stateWaitingCount,
|
&stateWaitingCount,
|
||||||
&stateLimiterRatio,
|
&stateLimiterRatio,
|
||||||
&lastAssignedUnix,
|
&lastAssignedUnix,
|
||||||
|
&cacheAffinityKey,
|
||||||
&cacheRequestCount,
|
&cacheRequestCount,
|
||||||
&cacheInputTokens,
|
&cacheInputTokens,
|
||||||
&cacheCachedInputTokens,
|
&cacheCachedInputTokens,
|
||||||
@@ -302,6 +311,7 @@ WHERE p.status = 'enabled'
|
|||||||
item.WaitingCount = maxFloat(queuedWaiting, stateWaitingCount)
|
item.WaitingCount = maxFloat(queuedWaiting, stateWaitingCount)
|
||||||
item.LastAssignedUnix = lastAssignedUnix
|
item.LastAssignedUnix = lastAssignedUnix
|
||||||
applyRuntimeCandidateCacheAffinity(&item, listOptions, runtimeCandidateCacheAffinityInput{
|
applyRuntimeCandidateCacheAffinity(&item, listOptions, runtimeCandidateCacheAffinityInput{
|
||||||
|
Key: cacheAffinityKey,
|
||||||
RequestCount: int(cacheRequestCount),
|
RequestCount: int(cacheRequestCount),
|
||||||
InputTokens: int(cacheInputTokens),
|
InputTokens: int(cacheInputTokens),
|
||||||
CachedInputTokens: int(cacheCachedInputTokens),
|
CachedInputTokens: int(cacheCachedInputTokens),
|
||||||
@@ -421,6 +431,7 @@ type runtimeCandidateLoadInput struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type runtimeCandidateCacheAffinityInput struct {
|
type runtimeCandidateCacheAffinityInput struct {
|
||||||
|
Key string
|
||||||
RequestCount int
|
RequestCount int
|
||||||
InputTokens int
|
InputTokens int
|
||||||
CachedInputTokens int
|
CachedInputTokens int
|
||||||
@@ -435,15 +446,38 @@ func normalizeListModelCandidatesOptions(modelType string, options ...ListModelC
|
|||||||
}
|
}
|
||||||
out := options[0]
|
out := options[0]
|
||||||
out.CacheAffinityKey = strings.TrimSpace(out.CacheAffinityKey)
|
out.CacheAffinityKey = strings.TrimSpace(out.CacheAffinityKey)
|
||||||
if out.CacheAffinityKey == "" || !cacheAffinityPolicyEnabled(out.CacheAffinityPolicy, modelType) {
|
out.CacheAffinityKeys = normalizedCacheAffinityKeys(out.CacheAffinityKey, out.CacheAffinityKeys)
|
||||||
|
if len(out.CacheAffinityKeys) > 0 {
|
||||||
|
out.CacheAffinityKey = out.CacheAffinityKeys[0]
|
||||||
|
}
|
||||||
|
if len(out.CacheAffinityKeys) == 0 || !cacheAffinityPolicyEnabled(out.CacheAffinityPolicy, modelType) {
|
||||||
out.CacheAffinityKey = ""
|
out.CacheAffinityKey = ""
|
||||||
|
out.CacheAffinityKeys = nil
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizedCacheAffinityKeys(primary string, keys []string) []string {
|
||||||
|
seen := map[string]bool{}
|
||||||
|
out := make([]string, 0, len(keys)+1)
|
||||||
|
for _, key := range append([]string{primary}, keys...) {
|
||||||
|
key = strings.TrimSpace(key)
|
||||||
|
if key == "" || seen[key] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[key] = true
|
||||||
|
out = append(out, key)
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyRuntimeCandidateCacheAffinity(candidate *RuntimeModelCandidate, options ListModelCandidatesOptions, input runtimeCandidateCacheAffinityInput) {
|
func applyRuntimeCandidateCacheAffinity(candidate *RuntimeModelCandidate, options ListModelCandidatesOptions, input runtimeCandidateCacheAffinityInput) {
|
||||||
|
key := strings.TrimSpace(input.Key)
|
||||||
|
if key == "" {
|
||||||
|
key = options.CacheAffinityKey
|
||||||
|
}
|
||||||
affinity := RuntimeCandidateCacheAffinity{
|
affinity := RuntimeCandidateCacheAffinity{
|
||||||
Key: options.CacheAffinityKey,
|
Key: key,
|
||||||
RequestCount: input.RequestCount,
|
RequestCount: input.RequestCount,
|
||||||
InputTokens: input.InputTokens,
|
InputTokens: input.InputTokens,
|
||||||
CachedInputTokens: input.CachedInputTokens,
|
CachedInputTokens: input.CachedInputTokens,
|
||||||
@@ -455,7 +489,7 @@ func applyRuntimeCandidateCacheAffinity(candidate *RuntimeModelCandidate, option
|
|||||||
minSamples := cacheAffinityMinSamples(options.CacheAffinityPolicy)
|
minSamples := cacheAffinityMinSamples(options.CacheAffinityPolicy)
|
||||||
hasObservedCachedHit := affinity.CachedInputTokens > 0 || affinity.LastHitRatio > 0
|
hasObservedCachedHit := affinity.CachedInputTokens > 0 || affinity.LastHitRatio > 0
|
||||||
hasSampledAffinity := input.RequestCount >= minSamples && affinity.EMAHitRatio > 0
|
hasSampledAffinity := input.RequestCount >= minSamples && affinity.EMAHitRatio > 0
|
||||||
if options.CacheAffinityKey == "" || input.RequestCount <= 0 || (!hasObservedCachedHit && !hasSampledAffinity) {
|
if key == "" || input.RequestCount <= 0 || (!hasObservedCachedHit && !hasSampledAffinity) {
|
||||||
candidate.CacheAffinity = affinity
|
candidate.CacheAffinity = affinity
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user