feat(retrieval): offer device benchmark
This commit is contained in:
1 parent
c37ffe4c66
commit
ef084f6fef
6 files changed
+154
-6
No files matched your search
@@ -24,7 +24,23 @@ function Read-InstallOptions {
|
|||||||
return @{
|
return @{
|
||||||
update_agents = Read-InstallBoolean -Prompt "Update selected agent CLIs ($Client)?" -Default $defaults.update_agents
|
update_agents = Read-InstallBoolean -Prompt "Update selected agent CLIs ($Client)?" -Default $defaults.update_agents
|
||||||
flashbang = Read-InstallBoolean -Prompt 'Enable the Flashbang notification hook?' -Default $defaults.flashbang
|
flashbang = Read-InstallBoolean -Prompt 'Enable the Flashbang notification hook?' -Default $defaults.flashbang
|
||||||
semantic_retrieval = Read-InstallBoolean -Prompt 'Enable Qwen3 embedding and reranking semantic retrieval?' -Default $defaults.semantic_retrieval
|
semantic_retrieval = Read-SemanticRetrievalOption -Default $defaults.semantic_retrieval -RepositoryRoot $RepositoryRoot -HomePath $HomePath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Read-SemanticRetrievalOption {
|
||||||
|
param([bool]$Default,[string]$RepositoryRoot,[string]$HomePath)
|
||||||
|
$hint = if ($Default) { 'Y/n/a' } else { 'y/N/a' }
|
||||||
|
while ($true) {
|
||||||
|
Write-Host "Enable Qwen3 embedding and reranking semantic retrieval? [$hint] " -NoNewline
|
||||||
|
$answer = [Console]::ReadLine()
|
||||||
|
if ($null -eq $answer) { throw 'Input ended before semantic retrieval was confirmed.' }
|
||||||
|
switch ($answer.Trim().ToLowerInvariant()) {
|
||||||
|
'' { return $Default }
|
||||||
|
{ $_ -in @('y', 'yes') } { return $true }
|
||||||
|
{ $_ -in @('n', 'no') } { return $false }
|
||||||
|
{ $_ -in @('a', 'auto') } { return Test-SemanticRetrievalDevice -RepositoryRoot $RepositoryRoot -HomePath $HomePath }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,28 @@ read_install_options() {
|
|||||||
[ "$dry_run" = false ] || return 0
|
[ "$dry_run" = false ] || return 0
|
||||||
update_agents=$(read_install_boolean "Update selected agent CLIs ($client)?" "$update_agents") || return
|
update_agents=$(read_install_boolean "Update selected agent CLIs ($client)?" "$update_agents") || return
|
||||||
flashbang=$(read_install_boolean 'Enable the Flashbang notification hook?' "$flashbang") || return
|
flashbang=$(read_install_boolean 'Enable the Flashbang notification hook?' "$flashbang") || return
|
||||||
semantic_retrieval=$(read_install_boolean 'Enable Qwen3 embedding and reranking semantic retrieval?' "$semantic_retrieval") || return
|
semantic_retrieval=$(read_semantic_retrieval_option "$semantic_retrieval") || return
|
||||||
|
}
|
||||||
|
|
||||||
|
read_semantic_retrieval_option() {
|
||||||
|
local default=$1 hint=y/N/a answer
|
||||||
|
[ "$default" = false ] || hint=Y/n/a
|
||||||
|
while :; do
|
||||||
|
printf 'Enable Qwen3 embedding and reranking semantic retrieval? [%s] ' "$hint" >&2
|
||||||
|
IFS= read -r answer || { printf 'Input ended before semantic retrieval was confirmed.\n' >&2; return 1; }
|
||||||
|
answer=${answer%$'\r'}
|
||||||
|
case "$answer" in
|
||||||
|
'') printf '%s\n' "$default"; return ;;
|
||||||
|
y|Y|yes|Yes|YES) printf 'true\n'; return ;;
|
||||||
|
n|N|no|No|NO) printf 'false\n'; return ;;
|
||||||
|
a|A|auto|Auto|AUTO)
|
||||||
|
if test_semantic_retrieval_device "$root" "$home_path"; then printf 'true\n'; return; fi
|
||||||
|
status=$?
|
||||||
|
[ "$status" -eq 1 ] && { printf 'false\n'; return; }
|
||||||
|
return "$status"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
global_npm_package() {
|
global_npm_package() {
|
||||||
|
|||||||
@@ -8,3 +8,11 @@ function Sync-SemanticRetrieval {
|
|||||||
& python @arguments
|
& python @arguments
|
||||||
if ($LASTEXITCODE -ne 0) { throw 'Semantic retrieval reconciliation failed.' }
|
if ($LASTEXITCODE -ne 0) { throw 'Semantic retrieval reconciliation failed.' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Test-SemanticRetrievalDevice {
|
||||||
|
param([Parameter(Mandatory=$true)][string]$RepositoryRoot,[Parameter(Mandatory=$true)][string]$HomePath,[switch]$Summary)
|
||||||
|
& python (Join-Path $PSScriptRoot 'semantic-retrieval.py') benchmark --root $RepositoryRoot --home $HomePath $(if ($Summary) { '--summary' }) | Write-Host
|
||||||
|
if ($LASTEXITCODE -eq 0) { return $true }
|
||||||
|
if ($LASTEXITCODE -eq 2) { return $false }
|
||||||
|
throw 'Semantic retrieval benchmark failed.'
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
FILES = ('server.py', 'requirements.txt')
|
FILES = ('server.py', 'benchmark.py', 'requirements.txt')
|
||||||
CUDA_INDEX = 'https://download.pytorch.org/whl/cu126'
|
CUDA_INDEX = 'https://download.pytorch.org/whl/cu126'
|
||||||
|
|
||||||
|
|
||||||
@@ -106,17 +106,55 @@ def sync(root, home, clients, enabled, dry_run, update, summary=False):
|
|||||||
save_state(state_path, state, False)
|
save_state(state_path, state, False)
|
||||||
|
|
||||||
|
|
||||||
|
def benchmark(root, home, dry_run, summary=False):
|
||||||
|
base = home / '.my-ai-configuration'
|
||||||
|
target = base / 'semantic-retrieval'
|
||||||
|
state_path = base / 'semantic-retrieval.json'
|
||||||
|
if dry_run:
|
||||||
|
if not summary:
|
||||||
|
print('DRYRUN benchmark semantic retrieval: Qwen3-Embedding-0.6B + Qwen3-Reranker-0.6B')
|
||||||
|
return True
|
||||||
|
state = load_state(state_path)
|
||||||
|
source = root / 'shared' / 'retrieval'
|
||||||
|
if target.exists() and not state['files']:
|
||||||
|
if not all((target / name).is_file() and digest(target / name) == digest(source / name) for name in FILES):
|
||||||
|
raise ValueError('Semantic retrieval directory exists without setup ownership; refusing to overwrite it.')
|
||||||
|
target.mkdir(parents=True, exist_ok=True)
|
||||||
|
for name in FILES:
|
||||||
|
shutil.copy2(source / name, target / name)
|
||||||
|
state['files'][name] = digest(target / name)
|
||||||
|
python = ensure_runtime(target, False)
|
||||||
|
result = subprocess.run([str(python), str(target / 'benchmark.py'), '--model-cache', str(target / 'model-cache')], check=True,
|
||||||
|
stdout=subprocess.PIPE, text=True, timeout=300)
|
||||||
|
outcome = json.loads(result.stdout)
|
||||||
|
if not isinstance(outcome, dict) or not isinstance(outcome.get('recommended'), bool):
|
||||||
|
raise ValueError('Semantic retrieval benchmark returned an invalid result.')
|
||||||
|
if outcome['recommended']:
|
||||||
|
save_state(state_path, state, False)
|
||||||
|
else:
|
||||||
|
shutil.rmtree(target)
|
||||||
|
state_path.unlink(missing_ok=True)
|
||||||
|
if not summary:
|
||||||
|
print('Semantic retrieval benchmark: ' + ('recommended' if outcome['recommended'] else 'not recommended') + f" (embedding {outcome['embeddingSeconds']}s, reranking {outcome['rerankingSeconds']}s)")
|
||||||
|
return outcome['recommended']
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('action', choices=('sync',))
|
parser.add_argument('action', choices=('sync', 'benchmark'))
|
||||||
parser.add_argument('--root', required=True, type=Path)
|
parser.add_argument('--root', required=True, type=Path)
|
||||||
parser.add_argument('--home', required=True, type=Path)
|
parser.add_argument('--home', required=True, type=Path)
|
||||||
parser.add_argument('--client', choices=('codex', 'claude', 'both'), required=True)
|
parser.add_argument('--client', choices=('codex', 'claude', 'both'))
|
||||||
parser.add_argument('--enabled', choices=('true', 'false'), required=True)
|
parser.add_argument('--enabled', choices=('true', 'false'))
|
||||||
parser.add_argument('--dry-run', action='store_true')
|
parser.add_argument('--dry-run', action='store_true')
|
||||||
parser.add_argument('--update', action='store_true')
|
parser.add_argument('--update', action='store_true')
|
||||||
parser.add_argument('--summary', action='store_true')
|
parser.add_argument('--summary', action='store_true')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
if args.action == 'benchmark':
|
||||||
|
recommended = benchmark(args.root.resolve(), args.home.resolve(), args.dry_run, args.summary)
|
||||||
|
raise SystemExit(0 if recommended else 2)
|
||||||
|
if args.client is None or args.enabled is None:
|
||||||
|
parser.error('sync requires --client and --enabled')
|
||||||
clients = ('codex', 'claude') if args.client == 'both' else (args.client,)
|
clients = ('codex', 'claude') if args.client == 'both' else (args.client,)
|
||||||
sync(args.root.resolve(), args.home.resolve(), clients, args.enabled == 'true', args.dry_run, args.update, args.summary)
|
sync(args.root.resolve(), args.home.resolve(), clients, args.enabled == 'true', args.dry_run, args.update, args.summary)
|
||||||
print('SEMANTIC RETRIEVAL: reconciliation complete' + (' (dry run)' if args.dry_run else ''))
|
print('SEMANTIC RETRIEVAL: reconciliation complete' + (' (dry run)' if args.dry_run else ''))
|
||||||
|
|||||||
@@ -9,3 +9,12 @@ sync_semantic_retrieval() {
|
|||||||
[ "$summary" != true ] || arguments+=(--summary)
|
[ "$summary" != true ] || arguments+=(--summary)
|
||||||
python3 "${arguments[@]}"
|
python3 "${arguments[@]}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test_semantic_retrieval_device() {
|
||||||
|
local root=$1 home_path=$2 summary=${3:-false}
|
||||||
|
local arguments=("$root/scripts/lib/semantic-retrieval.py" benchmark --root "$root" --home "$home_path")
|
||||||
|
[ "$summary" != true ] || arguments+=(--summary)
|
||||||
|
if python3 "${arguments[@]}" >&2; then return 0; fi
|
||||||
|
[ "$?" -eq 2 ] && return 1
|
||||||
|
return 2
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import statistics
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
def elapsed(call):
|
||||||
|
started = time.monotonic()
|
||||||
|
call()
|
||||||
|
return time.monotonic() - started
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('--model-cache', required=True, type=Path)
|
||||||
|
parser.add_argument('--max-seconds', type=float, default=float(os.environ.get('QWEN_BENCHMARK_MAX_SECONDS', '8')))
|
||||||
|
args = parser.parse_args()
|
||||||
|
if args.max_seconds <= 0:
|
||||||
|
raise ValueError('--max-seconds must be greater than zero.')
|
||||||
|
os.environ['HF_HOME'] = str(args.model_cache)
|
||||||
|
from server import shared_encoder, shared_reranker
|
||||||
|
|
||||||
|
encoder = shared_encoder()
|
||||||
|
reranker = shared_reranker()
|
||||||
|
query = 'Find the implementation that updates the selected client configuration.'
|
||||||
|
documents = ['Path: scripts/commands/install.py\n\n' + query] * 50
|
||||||
|
print('Benchmark: warming up embedding model...', file=sys.stderr, flush=True)
|
||||||
|
encoder.encode([query], prompt_name='query')
|
||||||
|
print('Benchmark: probing reranker speed...', file=sys.stderr, flush=True)
|
||||||
|
probe = elapsed(lambda: reranker.predict([(query, documents[0])], show_progress_bar=False))
|
||||||
|
if probe * len(documents) > args.max_seconds:
|
||||||
|
print(json.dumps({
|
||||||
|
'embeddingSeconds': None,
|
||||||
|
'rerankingSeconds': round(probe, 3),
|
||||||
|
'maxSeconds': args.max_seconds,
|
||||||
|
'recommended': False,
|
||||||
|
}))
|
||||||
|
return
|
||||||
|
print('Benchmark: measuring full retrieval batch...', file=sys.stderr, flush=True)
|
||||||
|
reranker.predict([(query, document) for document in documents], show_progress_bar=False)
|
||||||
|
embedding = statistics.median(elapsed(lambda: encoder.encode([query], prompt_name='query')) for _ in range(3))
|
||||||
|
reranking = statistics.median(elapsed(lambda: reranker.predict([(query, document) for document in documents], show_progress_bar=False)) for _ in range(3))
|
||||||
|
result = {
|
||||||
|
'embeddingSeconds': round(embedding, 3),
|
||||||
|
'rerankingSeconds': round(reranking, 3),
|
||||||
|
'maxSeconds': args.max_seconds,
|
||||||
|
'recommended': embedding <= args.max_seconds and reranking <= args.max_seconds,
|
||||||
|
}
|
||||||
|
print(json.dumps(result))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Reference in new issue
Block a user