SemanticCaches.jl ist eine sehr hackige Implementierung eines semantischen Caches für KI-Anwendungen, um bei wiederholten Anfragen Zeit und Geld zu sparen. Es ist nicht besonders schnell, da wir versuchen, API-Aufrufe zu verhindern, die sogar 20 Sekunden dauern können.
Beachten Sie, dass wir ein kleines BERT-Modell mit einer maximalen Blockgröße von 512 Token verwenden, um schnelle lokale Einbettungen zu ermöglichen, die auf einer CPU ausgeführt werden. Bei längeren Sätzen teilen wir sie in mehrere Abschnitte auf und berücksichtigen ihre durchschnittliche Einbettung, aber gehen Sie vorsichtig damit um! Die Latenz kann in die Höhe schießen und schlimmer werden, als wenn nur die ursprüngliche API aufgerufen wird.
Um SemanticCaches.jl zu installieren, fügen Sie das Paket einfach mit dem Julia-Paketmanager hinzu:
using Pkg;
Pkg . activate ( " . " )
Pkg . add ( " SemanticCaches " )
# # This line is very important to be able to download the models!!!
ENV [ " DATADEPS_ALWAYS_ACCEPT " ] = " true "
using SemanticCaches
sem_cache = SemanticCache ()
# First argument: the key must always match exactly, eg, model, temperature, etc
# Second argument: the input text to be compared with the cache, can be fuzzy matched
item = sem_cache ( " key1 " , " say hi! " ; verbose = 1 ) # notice the verbose flag it can 0,1,2 for different level of detail
if ! isvalid (item)
@info " cache miss! "
item . output = " expensive result X "
# Save the result to the cache for future reference
push! (sem_cache, item)
end
# If practice, long texts may take too long to embed even with our tiny model
# so let's not compare anything above 2000 tokens =~ 5000 characters (threshold of c. 100ms)
hash_cache = HashCache ()
input = " say hi "
input = " say hi " ^ 1000
active_cache = length (input) > 5000 ? hash_cache : sem_cache
item = active_cache ( " key1 " , input; verbose = 1 )
if ! isvalid (item)
@info " cache miss! "
item . output = " expensive result X "
push! (active_cache, item)
end
Das Hauptziel bei der Erstellung dieses Pakets bestand darin, teure API-Aufrufe für GenAI-Modelle zwischenzuspeichern.
Das System bietet exakte Übereinstimmung (schneller, HashCache
) und semantische Ähnlichkeitssuche (langsamer, SemanticCache
) von STRING-Eingaben. Darüber hinaus werden alle Anfragen zunächst mit einem „Cache-Schlüssel“ verglichen, der einen Schlüssel darstellt, der immer genau übereinstimmen muss, damit Anfragen als austauschbar gelten (z. B. gleiches Modell, gleicher Anbieter, gleiche Temperatur usw.). Sie müssen je nach Anwendungsfall den entsprechenden Cache-Schlüssel und die entsprechende Eingabe auswählen. Diese Standardauswahl für den Cache-Schlüssel sollte der Modellname sein.
Was passiert, wenn Sie den Cache aufrufen ( cache_key
und string_input
angeben)?
cache.items
gespeichert.cache_key
nach Indizes der entsprechenden Elemente in items
gesucht. Wenn cache_key
nicht gefunden wird, geben wir CachedItem
mit einem leeren output
zurück (d. h. isvalid(item) == false
).string_input
mithilfe eines winzigen BERT-Modells ein und normalisieren die Einbettungen (um den späteren Vergleich des Kosinusabstands zu erleichtern).min_similarity
Schwellenwert ist, geben wir das zwischengespeicherte Element zurück (die Ausgabe finden Sie im Feld item.output
). Wenn wir kein zwischengespeichertes Element gefunden haben, geben wir CachedItem
mit einem leeren output
zurück (d. h. isvalid(item) == false
). Sobald Sie die Antwort berechnet und in item.output
gespeichert haben, können Sie das Element in den Cache verschieben, indem Sie push!(cache, item)
aufrufen.
Basierend auf Ihrem Wissen über die durchgeführten API-Aufrufe müssen Sie Folgendes bestimmen: 1) den Cache-Schlüssel (separater Speicher von zwischengespeicherten Elementen, z. B. verschiedene Modelle oder Temperaturen) und 2) wie die HTTP-Anfrage in eine Zeichenfolge entpackt wird (z. B. entpacken und). Verknüpfen Sie die formatierten Nachrichteninhalte für die OpenAI-API.
Hier ist ein kurzer Überblick darüber, wie Sie SemanticCaches.jl mit PromptingTools.jl verwenden können.
using PromptingTools
using SemanticCaches
using HTTP
# # Define the new caching mechanism as a layer for HTTP
# # See documentation [here](https://juliaweb.github.io/HTTP.jl/stable/client/#Quick-Examples)
module MyCache
using HTTP, JSON3
using SemanticCaches
const SEM_CACHE = SemanticCache ()
const HASH_CACHE = HashCache ()
function cache_layer (handler)
return function (req; cache_key :: Union{AbstractString,Nothing} = nothing , kw ... )
# only apply the cache layer if the user passed `cache_key`
# we could also use the contents of the payload, eg, `cache_key = get(body, "model", "unknown")`
if req . method == " POST " && cache_key != = nothing
body = JSON3 . read ( copy (req . body))
if occursin ( " v1/chat/completions " , req . target)
# # We're in chat completion endpoint
input = join ([m[ " content " ] for m in body[ " messages " ]], " " )
elseif occursin ( " v1/embeddings " , req . target)
# # We're in embedding endpoint
input = body[ " input " ]
else
# # Skip, unknown API
return handler (req; kw ... )
end
# # Check the cache
@info " Check if we can cache this request ( $( length (input)) chars) "
active_cache = length (input) > 5000 ? HASH_CACHE : SEM_CACHE
item = active_cache ( " key1 " , input; verbose = 2 ) # change verbosity to 0 to disable detailed logs
if ! isvalid (item)
@info " Cache miss! Pinging the API "
# pass the request along to the next layer by calling `cache_layer` arg `handler`
resp = handler (req; kw ... )
item . output = resp
# Let's remember it for the next time
push! (active_cache, item)
end
# # Return the calculated or cached result
return item . output
end
# pass the request along to the next layer by calling `cache_layer` arg `handler`
# also pass along the trailing keyword args `kw...`
return handler (req; kw ... )
end
end
# Create a new client with the auth layer added
HTTP . @client [cache_layer]
end # module
# Let's push the layer globally in all HTTP.jl requests
HTTP . pushlayer! (MyCache . cache_layer)
# HTTP.poplayer!() # to remove it later
# Let's call the API
@time msg = aigenerate ( " What is the meaning of life? " ; http_kwargs = (; cache_key = " key1 " ))
# The first call will be slow as usual, but any subsequent call should be pretty quick - try it a few times!
Sie können es auch für Einbettungen verwenden, z.
@time msg = aiembed ( " how is it going? " ; http_kwargs = (; cache_key = " key2 " )) # 0.7s
@time msg = aiembed ( " how is it going? " ; http_kwargs = (; cache_key = " key2 " )) # 0.02s
# Even with a tiny difference (no question mark), it still picks the right cache
@time msg = aiembed ( " how is it going " ; http_kwargs = (; cache_key = " key2 " )) # 0.02s
Sie können die Cache-Ebene entfernen, indem Sie HTTP.poplayer!()
aufrufen (und sie erneut hinzufügen, wenn Sie einige Änderungen vorgenommen haben).
Sie können den Cache untersuchen, indem Sie MyCache.SEM_CACHE
aufrufen (z. B. MyCache.SEM_CACHE.items[1]
).
Wie ist die Leistung?
Der Großteil der Zeit wird für 1) winzige Einbettungen (für große Texte, z. B. Tausende von Token) und für die Berechnung der Kosinusähnlichkeit (für große Caches, z. B. über 10.000 Elemente) aufgewendet.
Als Referenz: Das Einbetten kleinerer Texte wie z. B. einzubettender Fragen dauert nur wenige Millisekunden. Das Einbetten von 2000 Token kann zwischen 50 und 100 ms dauern.
Was das Caching-System betrifft, gibt es viele Sperren, um Fehler zu vermeiden, aber der Overhead ist immer noch vernachlässigbar – ich habe Experimente mit 100.000 aufeinanderfolgenden Einfügungen durchgeführt und die Zeit pro Element betrug nur wenige Millisekunden (dominiert durch die Kosinusähnlichkeit). Wenn Ihr Engpass in der Kosinusähnlichkeitsberechnung liegt (ca. 4 ms für 100.000 Elemente), sollten Sie erwägen, Vektoren in eine Matrix für kontinuierlichen Speicher zu verschieben und/oder boolesche Einbettungen mit Hamming-Distanz zu verwenden (XOR-Operator, ca. Größenordnung schneller).
Alles in allem ist das System für normale Arbeitslasten mit Tausenden von zwischengespeicherten Elementen schneller als nötig. Es ist wahrscheinlicher, dass Sie GC- und Speicherprobleme haben, wenn Ihre Nutzlasten groß sind (erwägen Sie die Auslagerung auf die Festplatte), als dass Sie mit Rechengrenzen konfrontiert werden. Denken Sie daran, dass die Motivation darin besteht, API-Aufrufe zu verhindern, die zwischen 1 und 20 Sekunden dauern!
Wie misst man die Zeit, die für X benötigt wird?
Schauen Sie sich die Beispielausschnitte unten an – wählen Sie den Teil aus, der Sie interessiert.
sem_cache = SemanticCache ()
# First argument: the key must always match exactly, eg, model, temperature, etc
# Second argument: the input text to be compared with the cache, can be fuzzy matched
item = sem_cache ( " key1 " , " say hi! " ; verbose = 1 ) # notice the verbose flag it can 0,1,2 for different level of detail
if ! isvalid (item)
@info " cache miss! "
item . output = " expensive result X "
# Save the result to the cache for future reference
push! (sem_cache, item)
end
Nur Einbettung (um den min_similarity
Schwellenwert anzupassen oder die Einbettung zeitlich festzulegen)
using SemanticCaches . FlashRank : embed
using SemanticCaches : EMBEDDER
@time res = embed (EMBEDDER, " say hi " )
# 0.000903 seconds (104 allocations: 19.273 KiB)
# see res.elapsed or res.embeddings
# long inputs (split into several chunks and then combining the embeddings)
@time embed (EMBEDDER, " say hi " ^ 1000 )
# 0.032148 seconds (8.11 k allocations: 662.656 KiB)
Wie lege ich den min_similarity
Schwellenwert fest?
Sie können den Schwellenwert min_similarity
festlegen, indem Sie kwarg active_cache("key1", input; verbose=2, min_similarity=0.95)
hinzufügen.
Der Standardwert ist 0,95, was einem sehr hohen Schwellenwert entspricht. Aus praktischen Gründen würde ich ~0,9 empfehlen. Wenn Sie mit Tippfehlern rechnen, können Sie den Wert sogar noch etwas senken (z. B. 0,85).
Warnung
Seien Sie vorsichtig mit Ähnlichkeitsschwellenwerten. Es ist schwer, superkurze Sequenzen gut einzubetten! Möglicherweise möchten Sie den Schwellenwert je nach Länge der Eingabe anpassen. Testen Sie sie immer mit Ihren Eingaben!!
Wenn Sie die Kosinusähnlichkeit berechnen möchten, denken Sie daran, zuerst die Einbettungen zu normalize
oder das Skalarprodukt durch die Normen zu dividieren.
using SemanticCaches . LinearAlgebra : normalize, norm, dot
cosine_similarity = dot (r1 . embeddings, r2 . embeddings) / ( norm (r1 . embeddings) * norm (r2 . embeddings))
# remember that 1 is the best similarity, -1 is the exact opposite
Sie können verschiedene Eingaben vergleichen, um den besten Schwellenwert für Ihre Anwendungsfälle zu ermitteln
emb1 = embed (EMBEDDER, " How is it going? " ) |> x -> vec (x . embeddings) |> normalize
emb2 = embed (EMBEDDER, " How is it goin'? " ) |> x -> vec (x . embeddings) |> normalize
dot (emb1, emb2) # 0.944
emb1 = embed (EMBEDDER, " How is it going? " ) |> x -> vec (x . embeddings) |> normalize
emb2 = embed (EMBEDDER, " How is it goin' " ) |> x -> vec (x . embeddings) |> normalize
dot (emb1, emb2) # 0.920
Wie debugge ich es?
Aktivieren Sie die ausführliche Protokollierung, indem Sie kwarg verbose = 2
hinzufügen, z. B. item = active_cache("key1", input; verbose=2)
.
[ ] Zeitbasierte Cache-Gültigkeit [ ] Beschleunigen Sie den Einbettungsprozess/erwägen Sie die Vorverarbeitung der Eingaben [ ] Native Integration mit PromptingTools und den API-Schemas