يعد SemanticCaches.jl تطبيقًا مبتكرًا للغاية لذاكرة التخزين المؤقت الدلالية لتطبيقات الذكاء الاصطناعي لتوفير الوقت والمال مع الطلبات المتكررة. إنه ليس سريعًا بشكل خاص، لأننا نحاول منع استدعاءات واجهة برمجة التطبيقات (API) التي يمكن أن تستغرق حتى 20 ثانية.
لاحظ أننا نستخدم نموذج BERT صغيرًا بحد أقصى لحجم القطعة يبلغ 512 رمزًا لتوفير عمليات تضمين محلية سريعة تعمل على وحدة المعالجة المركزية. بالنسبة للجمل الأطول، نقوم بتقسيمها إلى عدة أجزاء ونأخذ في الاعتبار متوسط تضمينها، ولكن استخدمها بعناية! يمكن أن يصبح زمن الوصول صاروخيًا ويصبح أسوأ من مجرد استدعاء واجهة برمجة التطبيقات الأصلية.
لتثبيت SemanticCaches.jl، ما عليك سوى إضافة الحزمة باستخدام مدير حزم 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
كان الهدف الأساسي من بناء هذه الحزمة هو تخزين استدعاءات واجهة برمجة التطبيقات (API) باهظة الثمن مؤقتًا لنماذج GenAI.
يوفر النظام مطابقة تامة (أسرع، HashCache
) والبحث عن التشابه الدلالي (أبطأ، SemanticCache
) لمدخلات STRING. بالإضافة إلى ذلك، تتم مقارنة جميع الطلبات أولاً على "مفتاح ذاكرة التخزين المؤقت"، الذي يقدم مفتاحًا يجب أن يتطابق دائمًا تمامًا حتى تعتبر الطلبات قابلة للتبديل (على سبيل المثال، نفس الطراز، نفس المزود، نفس درجة الحرارة، إلخ). تحتاج إلى اختيار مفتاح ذاكرة التخزين المؤقت والإدخال المناسبين وفقًا لحالة الاستخدام الخاصة بك. يجب أن يكون هذا الاختيار الافتراضي لمفتاح ذاكرة التخزين المؤقت هو اسم الطراز.
ماذا يحدث عند استدعاء ذاكرة التخزين المؤقت (توفير cache_key
و string_input
)؟
cache.items
متجهة.cache_key
للعثور على فهارس العناصر المقابلة في items
. إذا لم يتم العثور على cache_key
، فإننا نعيد CachedItem
مع حقل output
فارغ (على سبيل المثال، isvalid(item) == false
).string_input
باستخدام نموذج BERT صغير وقمنا بتطبيع التضمينات (لتسهيل مقارنة مسافة جيب التمام لاحقًا).min_similarity
، فإننا نعيد العنصر المخزن مؤقتًا (يمكن العثور على الإخراج في الحقل item.output
). إذا لم نعثر على أي عنصر مخبأ، فإننا نعيد CachedItem
مع حقل output
فارغ (على سبيل المثال، isvalid(item) == false
). بمجرد حساب الاستجابة وحفظها في item.output
، يمكنك دفع العنصر إلى ذاكرة التخزين المؤقت عن طريق استدعاء push!(cache, item)
.
استنادًا إلى معرفتك باستدعاءات واجهة برمجة التطبيقات (API) التي تم إجراؤها، يتعين عليك تحديد: 1) مفتاح ذاكرة التخزين المؤقت (مخزن منفصل للعناصر المخزنة مؤقتًا، على سبيل المثال، نماذج أو درجات حرارة مختلفة) و2) كيفية فك ضغط طلب HTTP في سلسلة (على سبيل المثال، فك و الانضمام إلى محتويات الرسالة المنسقة لـ OpenAI API).
فيما يلي مخطط مختصر لكيفية استخدام SemanticCaches.jl مع 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!
يمكنك أيضًا استخدامه للتضمين، على سبيل المثال،
@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
يمكنك إزالة طبقة ذاكرة التخزين المؤقت عن طريق استدعاء HTTP.poplayer!()
(وإضافتها مرة أخرى إذا قمت بإجراء بعض التغييرات).
يمكنك التحقق من ذاكرة التخزين المؤقت عن طريق استدعاء MyCache.SEM_CACHE
(على سبيل المثال، MyCache.SEM_CACHE.items[1]
).
كيف هو الأداء؟
سيتم قضاء معظم الوقت في 1) التضمينات الصغيرة (للنصوص الكبيرة، على سبيل المثال، آلاف الرموز المميزة) وفي حساب تشابه جيب التمام (للذاكرة المؤقتة الكبيرة، على سبيل المثال، أكثر من 10 آلاف عنصر).
كمرجع، فإن تضمين نصوص أصغر مثل الأسئلة المراد تضمينها لا يستغرق سوى بضعة أجزاء من الثانية. يمكن أن يستغرق تضمين 2000 رمزًا ما بين 50 إلى 100 مللي ثانية.
عندما يتعلق الأمر بنظام التخزين المؤقت، هناك العديد من الأقفال لتجنب الأخطاء، ولكن الحمل لا يزال ضئيلًا - لقد أجريت تجارب باستخدام 100 ألف إدخال متسلسل وكان الوقت لكل عنصر بضعة مللي ثانية فقط (يهيمن عليها تشابه جيب التمام). إذا كان عنق الزجاجة الخاص بك في حساب تشابه جيب التمام (حوالي 4 مللي ثانية لعناصر 100 ألف)، ففكر في نقل المتجهات إلى مصفوفة للذاكرة المستمرة و/أو استخدم التضمينات المنطقية مع مسافة هامينج (مشغل XOR، ج. تسريع ترتيب الحجم).
وبشكل عام، يعد النظام أسرع من اللازم لأحمال العمل العادية التي تحتوي على آلاف العناصر المخزنة مؤقتًا. من المرجح أن تواجه مشكلات في GC والذاكرة إذا كانت حمولاتك كبيرة (فكر في التبديل إلى القرص) بدلاً من مواجهة حدود الحوسبة. تذكر أن الدافع هو منع مكالمات API التي تستغرق ما بين 1 إلى 20 ثانية!
كيف يمكن قياس الوقت المستغرق للقيام بـ X؟
ألقِ نظرة على مقتطفات الأمثلة أدناه - حدد أي جزء يهمك فيه.
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
التضمين فقط (لضبط الحد min_similarity
أو لتوقيت التضمين)
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)
كيفية تعيين الحد الأدنى min_similarity
؟
يمكنك تعيين عتبة min_similarity
عن طريق إضافة kwarg active_cache("key1", input; verbose=2, min_similarity=0.95)
.
القيمة الافتراضية هي 0.95، وهي عتبة عالية جدًا. لأغراض عملية، أوصي بـ ~0.9. إذا كنت تتوقع بعض الأخطاء المطبعية، فيمكنك الانخفاض قليلاً (على سبيل المثال، 0.85).
تحذير
كن حذرا مع عتبات التشابه. من الصعب تضمين تسلسلات قصيرة جدًا بشكل جيد! قد ترغب في ضبط الحد اعتمادًا على طول الإدخال. اختبرهم دائمًا بمدخلاتك !!
إذا كنت تريد حساب تشابه جيب التمام، فتذكر normalize
التضمينات أولاً أو قسمة حاصل الضرب النقطي على القواعد.
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
يمكنك مقارنة المدخلات المختلفة لتحديد الحد الأفضل لحالات الاستخدام الخاصة بك
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
كيفية تصحيح ذلك؟
تمكين التسجيل المطول عن طريق إضافة verbose = 2
، على سبيل المثال، item = active_cache("key1", input; verbose=2)
.
[ ] صلاحية ذاكرة التخزين المؤقت المستندة إلى الوقت [ ] تسريع عملية التضمين / مراعاة المعالجة المسبقة للمدخلات [ ] التكامل الأصلي مع PromptingTools ومخططات واجهة برمجة التطبيقات