SemanticCaches.jl adalah implementasi cache semantik yang sangat rumit untuk aplikasi AI guna menghemat waktu dan uang dengan permintaan berulang. Ini tidak terlalu cepat, karena kami mencoba mencegah panggilan API yang bisa memakan waktu bahkan 20 detik.
Perhatikan bahwa kami menggunakan model BERT kecil dengan ukuran potongan maksimum 512 token untuk menyediakan penyematan lokal cepat yang berjalan pada CPU. Untuk kalimat yang lebih panjang, kami membaginya menjadi beberapa bagian dan mempertimbangkan rata-rata penyematannya, tetapi gunakan dengan hati-hati! Latensinya bisa meroket dan menjadi lebih buruk daripada sekadar memanggil API asli.
Untuk menginstal SemanticCaches.jl, cukup tambahkan paket menggunakan manajer paket Julia:
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
Tujuan utama pembuatan paket ini adalah untuk menyimpan panggilan API yang mahal ke model GenAI dalam cache.
Sistem ini menawarkan pencocokan tepat (lebih cepat, HashCache
) dan pencarian kesamaan semantik (lebih lambat, SemanticCache
) dari masukan STRING. Selain itu, semua permintaan pertama-tama dibandingkan pada “kunci cache”, yang menyajikan kunci yang harus selalu sama persis agar permintaan dianggap dapat dipertukarkan (misalnya, model yang sama, penyedia yang sama, suhu yang sama, dll). Anda harus memilih kunci cache dan input yang sesuai tergantung pada kasus penggunaan Anda. Pilihan default untuk kunci cache ini adalah nama model.
Apa yang terjadi jika Anda memanggil cache (menyediakan cache_key
dan string_input
)?
cache.items
.cache_key
dicari untuk menemukan indeks item terkait di items
. Jika cache_key
tidak ditemukan, kami mengembalikan CachedItem
dengan kolom output
kosong (yaitu, isvalid(item) == false
).string_input
menggunakan model BERT kecil dan menormalkan penyematannya (untuk memudahkan membandingkan jarak kosinus nanti).min_similarity
, kami mengembalikan item yang di-cache (Outputnya dapat ditemukan di bidang item.output
). Jika kami belum menemukan item yang di-cache, kami mengembalikan CachedItem
dengan kolom output
kosong (yaitu, isvalid(item) == false
). Setelah Anda menghitung respons dan menyimpannya di item.output
, Anda dapat memasukkan item ke cache dengan memanggil push!(cache, item)
.
Berdasarkan pengetahuan Anda tentang panggilan API yang dilakukan, Anda perlu menentukan: 1) kunci cache (penyimpanan item cache yang terpisah, misalnya, model atau suhu yang berbeda) dan 2) cara membongkar permintaan HTTP ke dalam string (misalnya, membuka bungkus dan bergabung dengan konten pesan yang diformat untuk OpenAI API).
Berikut ini garis besar singkat tentang bagaimana Anda dapat menggunakan SemanticCaches.jl dengan PromptingTools.jl.
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!
Anda juga dapat menggunakannya untuk penyematan, misalnya,
@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
Anda dapat menghapus lapisan cache dengan memanggil HTTP.poplayer!()
(dan menambahkannya lagi jika Anda membuat beberapa perubahan).
Anda dapat menyelidiki cache dengan memanggil MyCache.SEM_CACHE
(misalnya, MyCache.SEM_CACHE.items[1]
).
Bagaimana kinerjanya?
Sebagian besar waktu akan dihabiskan dalam 1) penyematan kecil (untuk teks besar, misalnya ribuan token) dan dalam menghitung kesamaan kosinus (untuk cache besar, misalnya lebih dari 10 ribu item).
Sebagai referensi, menyematkan teks yang lebih kecil seperti pertanyaan hanya membutuhkan waktu beberapa milidetik. Menyematkan 2000 token dapat memakan waktu antara 50-100 md.
Ketika datang ke sistem caching, ada banyak kunci untuk menghindari kesalahan, namun overhead masih dapat diabaikan - Saya menjalankan percobaan dengan penyisipan berurutan 100k dan waktu per item hanya beberapa milidetik (didominasi oleh kesamaan kosinus). Jika hambatan Anda ada dalam penghitungan kesamaan kosinus (c. 4 md untuk 100 ribu item), pertimbangkan untuk memindahkan vektor ke dalam matriks untuk memori berkelanjutan dan/atau gunakan penyematan Boolean dengan jarak Hamming (operator XOR, c. percepatan urutan besarnya).
Secara keseluruhan, sistem ini lebih cepat dari yang diperlukan untuk beban kerja normal dengan ribuan item yang di-cache. Anda lebih mungkin mengalami masalah GC dan memori jika payload Anda besar (pertimbangkan untuk menukar ke disk) daripada menghadapi batasan komputasi. Ingatlah bahwa motivasinya adalah untuk mencegah panggilan API yang memakan waktu antara 1-20 detik!
Bagaimana mengukur waktu yang diperlukan untuk melakukan X?
Lihat contoh cuplikan di bawah - waktu bagian mana pun yang Anda minati.
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
Hanya menyematkan (untuk menyetel ambang batas min_similarity
atau mengatur waktu penyematan)
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)
Bagaimana cara menetapkan ambang batas min_similarity
?
Anda dapat mengatur ambang batas min_similarity
dengan menambahkan kwarg active_cache("key1", input; verbose=2, min_similarity=0.95)
.
Standarnya adalah 0,95, yang merupakan ambang batas yang sangat tinggi. Untuk tujuan praktis, saya akan merekomendasikan ~0,9. Jika Anda mengharapkan beberapa kesalahan ketik, Anda dapat menurunkannya sedikit lagi (misalnya, 0,85).
Peringatan
Hati-hati dengan ambang batas kesamaan. Sulit untuk menyematkan urutan super pendek dengan baik! Anda mungkin ingin menyesuaikan ambang batas tergantung pada panjang masukan. Selalu uji mereka dengan masukan Anda!!
Jika Anda ingin menghitung kesamaan kosinus, ingatlah untuk normalize
embeddings terlebih dahulu atau membagi hasil perkalian titik dengan norma.
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
Anda dapat membandingkan masukan yang berbeda untuk menentukan ambang batas terbaik untuk kasus penggunaan Anda
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
Bagaimana cara men-debugnya?
Aktifkan logging verbose dengan menambahkan kwarg verbose = 2
, misalnya item = active_cache("key1", input; verbose=2)
.
[ ] Validitas cache berbasis waktu [ ] Mempercepat proses penyematan / mempertimbangkan pra-pemrosesan input [ ] Integrasi asli dengan PromptingTools dan skema API