SemanticCaches.jl은 반복되는 요청으로 시간과 비용을 절약하기 위해 AI 애플리케이션을 위한 의미론적 캐시를 매우 해킹적으로 구현한 것입니다. 20초라도 걸릴 수 있는 API 호출을 방지하려고 하기 때문에 특별히 빠르지는 않습니다.
CPU에서 실행되는 빠른 로컬 임베딩을 제공하기 위해 최대 청크 크기가 512개 토큰인 작은 BERT 모델을 사용하고 있습니다. 더 긴 문장의 경우 문장을 여러 덩어리로 나누고 평균 임베딩을 고려하지만 신중하게 사용하세요! 지연 시간은 급증할 수 있으며 단순히 원래 API를 호출하는 것보다 더 악화될 수 있습니다.
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
이 패키지를 구축하는 주요 목적은 GenAI 모델에 대한 값비싼 API 호출을 캐시하는 것이었습니다.
시스템은 STRING 입력의 정확한 일치(빠른 HashCache
) 및 의미론적 유사성 조회(느린 SemanticCache
)를 제공합니다. 또한 모든 요청은 먼저 "캐시 키"에서 비교됩니다. 이는 요청이 상호 교환 가능한 것으로 간주되도록 항상 정확히 일치해야 하는 키를 제공합니다(예: 동일한 모델, 동일한 공급자, 동일한 온도 등). 사용 사례에 따라 적절한 캐시 키와 입력을 선택해야 합니다. 캐시 키에 대한 기본 선택은 모델 이름이어야 합니다.
캐시를 호출하면( cache_key
및 string_input
제공) 어떻게 되나요?
cache.items
에 저장됩니다.cache_key
조회하여 items
에서 해당 항목의 인덱스를 찾습니다. cache_key
찾을 수 없으면 빈 output
필드(예: isvalid(item) == false
)와 함께 CachedItem
반환합니다.string_input
을 삽입하고 삽입을 정규화합니다(나중에 코사인 거리를 더 쉽게 비교할 수 있도록).min_similarity
임계값보다 높으면 캐시된 항목을 반환합니다(출력은 item.output
필드에서 찾을 수 있음). 캐시된 항목을 찾지 못한 경우 빈 output
필드(예: isvalid(item) == false
)와 함께 CachedItem
반환합니다. 응답을 계산하고 item.output
에 저장하면 push!(cache, item)
호출하여 항목을 캐시에 푸시할 수 있습니다.
수행된 API 호출에 대한 지식을 바탕으로 다음을 결정해야 합니다. 1) 캐시 키(캐시된 항목의 별도 저장소, 예: 다른 모델 또는 온도) 및 2) HTTP 요청을 문자열로 압축 해제하는 방법(예: unwrap 및 OpenAI API에 대한 형식이 지정된 메시지 내용에 참여하세요.
PromptingTools.jl과 함께 SemanticCaches.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,000개 이상의 항목)에 소요됩니다.
참고로 질문과 같은 작은 텍스트를 삽입하는 데는 몇 밀리초밖에 걸리지 않습니다. 2000개의 토큰을 삽입하는 데는 50~100ms가 걸릴 수 있습니다.
캐싱 시스템의 경우 오류를 피하기 위해 많은 잠금이 있지만 오버헤드는 여전히 미미합니다. 100,000개의 순차적 삽입으로 실험을 실행했는데 항목당 시간은 단 몇 밀리초였습니다(코사인 유사성이 지배적임). 코사인 유사성 계산(100,000개 항목의 경우 약 4ms)에 병목 현상이 발생하는 경우 연속 메모리를 위해 벡터를 행렬로 이동하거나 해밍 거리(XOR 연산자, c. 속도 증가)가 있는 부울 임베딩을 사용하는 것을 고려하세요.
전체적으로 시스템은 수천 개의 캐시된 항목이 있는 일반 작업 부하에 필요한 것보다 빠릅니다. 페이로드가 큰 경우(디스크로 교체 고려) 컴퓨팅 한계에 직면하는 것보다 GC 및 메모리 문제가 발생할 가능성이 더 높습니다. 동기는 1~20초 정도 소요되는 API 호출을 방지하는 것임을 기억하세요!
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
임계값을 설정하는 방법은 무엇입니까?
kwarg active_cache("key1", input; verbose=2, min_similarity=0.95)
추가하여 min_similarity
임계값을 설정할 수 있습니다.
기본값은 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
디버깅하는 방법?
kwarg verbose = 2
추가하여 자세한 로깅을 활성화합니다(예: item = active_cache("key1", input; verbose=2)
.
[ ] 시간 기반 캐시 유효성 [ ] 임베딩 프로세스 속도 향상/입력 전처리 고려 [ ] PromptingTools 및 API 스키마와의 기본 통합