sweet-search 0.0.1 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/NOTICE +23 -0
- package/core/cli.js +51 -0
- package/core/config.js +27 -0
- package/core/embedding/embedding-cache.js +467 -0
- package/core/embedding/embedding-local-model.js +845 -0
- package/core/embedding/embedding-remote.js +492 -0
- package/core/embedding/embedding-service.js +712 -0
- package/core/embedding/embedding-telemetry.js +219 -0
- package/core/embedding/index.js +40 -0
- package/core/graph/community-detector.js +294 -0
- package/core/graph/graph-expansion.js +839 -0
- package/core/graph/graph-extractor.js +2304 -0
- package/core/graph/graph-search.js +2148 -0
- package/core/graph/hcgs-generator.js +666 -0
- package/core/graph/index.js +16 -0
- package/core/graph/leiden-algorithm.js +547 -0
- package/core/graph/relationship-resolver.js +366 -0
- package/core/graph/repo-map.js +408 -0
- package/core/graph/summary-manager.js +549 -0
- package/core/indexing/artifact-builder.js +1054 -0
- package/core/indexing/ast-chunker.js +709 -0
- package/core/indexing/chunking/chunk-builder.js +170 -0
- package/core/indexing/chunking/markdown-chunker.js +503 -0
- package/core/indexing/chunking/plaintext-chunker.js +104 -0
- package/core/indexing/dedup/dedup-phase.js +159 -0
- package/core/indexing/dedup/exemplar-selector.js +65 -0
- package/core/indexing/document-chunker.js +56 -0
- package/core/indexing/incremental-parser.js +390 -0
- package/core/indexing/incremental-tracker.js +761 -0
- package/core/indexing/index-codebase-v21.js +472 -0
- package/core/indexing/index-maintainer.mjs +1674 -0
- package/core/indexing/index.js +90 -0
- package/core/indexing/indexer-ann.js +1077 -0
- package/core/indexing/indexer-build.js +742 -0
- package/core/indexing/indexer-phases.js +800 -0
- package/core/indexing/indexer-pool.js +764 -0
- package/core/indexing/indexer-sparse-gram.js +98 -0
- package/core/indexing/indexer-utils.js +536 -0
- package/core/indexing/indexer-worker.js +148 -0
- package/core/indexing/li-skip-policy.js +225 -0
- package/core/indexing/merkle-tracker.js +244 -0
- package/core/indexing/model-pool.js +166 -0
- package/core/infrastructure/code-graph-repository.js +120 -0
- package/core/infrastructure/codebase-repository.js +131 -0
- package/core/infrastructure/config/dedup.js +54 -0
- package/core/infrastructure/config/embedding.js +298 -0
- package/core/infrastructure/config/graph.js +80 -0
- package/core/infrastructure/config/index.js +82 -0
- package/core/infrastructure/config/indexing.js +8 -0
- package/core/infrastructure/config/platform.js +254 -0
- package/core/infrastructure/config/ranking.js +221 -0
- package/core/infrastructure/config/search.js +396 -0
- package/core/infrastructure/config/translation.js +89 -0
- package/core/infrastructure/config/vector-store.js +114 -0
- package/core/infrastructure/constants.js +86 -0
- package/core/infrastructure/coreml-cascade.js +909 -0
- package/core/infrastructure/coreml-cascade.json +46 -0
- package/core/infrastructure/coreml-provider.js +81 -0
- package/core/infrastructure/db-utils.js +69 -0
- package/core/infrastructure/dedup-hashing.js +83 -0
- package/core/infrastructure/hardware-capability.js +332 -0
- package/core/infrastructure/index.js +104 -0
- package/core/infrastructure/language-patterns/maps.js +121 -0
- package/core/infrastructure/language-patterns/registry-core.js +323 -0
- package/core/infrastructure/language-patterns/registry-data-query.js +155 -0
- package/core/infrastructure/language-patterns/registry-object-oriented.js +285 -0
- package/core/infrastructure/language-patterns/registry-tooling.js +240 -0
- package/core/infrastructure/language-patterns/registry-web-style.js +143 -0
- package/core/infrastructure/language-patterns/registry.js +19 -0
- package/core/infrastructure/language-patterns.js +141 -0
- package/core/infrastructure/llm-provider.js +733 -0
- package/core/infrastructure/manifest.json +46 -0
- package/core/infrastructure/maxsim.wasm +0 -0
- package/core/infrastructure/model-fetcher.js +423 -0
- package/core/infrastructure/model-registry.js +214 -0
- package/core/infrastructure/native-inference.js +587 -0
- package/core/infrastructure/native-resolver.js +187 -0
- package/core/infrastructure/native-sparse-gram.js +257 -0
- package/core/infrastructure/native-tokenizer.js +160 -0
- package/core/infrastructure/onnx-mutex.js +45 -0
- package/core/infrastructure/onnx-session-utils.js +261 -0
- package/core/infrastructure/ort-pipeline.js +111 -0
- package/core/infrastructure/project-detector.js +102 -0
- package/core/infrastructure/quantization.js +410 -0
- package/core/infrastructure/simd-distance.js +502 -0
- package/core/infrastructure/simd-distance.wasm +0 -0
- package/core/infrastructure/tree-sitter-provider.js +665 -0
- package/core/infrastructure/webgpu-maxsim.js +222 -0
- package/core/query/index.js +35 -0
- package/core/query/intent-detector.js +201 -0
- package/core/query/intent-router.js +156 -0
- package/core/query/query-router-catboost.js +222 -0
- package/core/query/query-router-ml.js +266 -0
- package/core/query/query-router.js +213 -0
- package/core/ranking/cascaded-scorer.js +379 -0
- package/core/ranking/flashrank.js +810 -0
- package/core/ranking/index.js +49 -0
- package/core/ranking/late-interaction-index.js +2383 -0
- package/core/ranking/late-interaction-model.js +812 -0
- package/core/ranking/local-reranker.js +374 -0
- package/core/ranking/mmr.js +379 -0
- package/core/ranking/quality-scorer.js +363 -0
- package/core/search/context-expander.js +1167 -0
- package/core/search/dedup/sibling-expander.js +327 -0
- package/core/search/index.js +16 -0
- package/core/search/search-boost.js +259 -0
- package/core/search/search-cli.js +544 -0
- package/core/search/search-format.js +282 -0
- package/core/search/search-fusion.js +327 -0
- package/core/search/search-hybrid.js +204 -0
- package/core/search/search-pattern-chunks.js +337 -0
- package/core/search/search-pattern-planner.js +439 -0
- package/core/search/search-pattern-prefilter.js +412 -0
- package/core/search/search-pattern-ripgrep.js +663 -0
- package/core/search/search-pattern.js +463 -0
- package/core/search/search-postprocess.js +452 -0
- package/core/search/search-semantic.js +706 -0
- package/core/search/search-server.js +554 -0
- package/core/search/session-daemon-prewarm.mjs +164 -0
- package/core/search/session-warmup.js +595 -0
- package/core/search/sweet-search.js +632 -0
- package/core/search/warmup-metrics.js +532 -0
- package/core/start-server.js +6 -0
- package/core/training/query-router/features/extractor.js +762 -0
- package/core/training/query-router/features/multilingual-patterns.js +431 -0
- package/core/training/query-router/features/text-segmenter.js +303 -0
- package/core/training/query-router/features/unicode-utils.js +383 -0
- package/core/training/query-router/output/v45_router_d4.js +11521 -0
- package/core/training/query-router/output/v46_router_d4.js +11498 -0
- package/core/vector-store/binary-heap.js +227 -0
- package/core/vector-store/binary-hnsw-index.js +1004 -0
- package/core/vector-store/float-vector-store.js +234 -0
- package/core/vector-store/hnsw-index.js +580 -0
- package/core/vector-store/index.js +39 -0
- package/core/vector-store/seismic-index.js +498 -0
- package/core/vocabulary/index.js +84 -0
- package/core/vocabulary/vocab-constants.js +20 -0
- package/core/vocabulary/vocab-miner-extractors.js +375 -0
- package/core/vocabulary/vocab-miner-nl.js +404 -0
- package/core/vocabulary/vocab-miner-utils.js +146 -0
- package/core/vocabulary/vocab-miner.js +574 -0
- package/core/vocabulary/vocab-prewarm-cli.js +110 -0
- package/core/vocabulary/vocab-ranker.js +492 -0
- package/core/vocabulary/vocab-warmer.js +523 -0
- package/core/vocabulary/vocab-warmup-orchestrator.js +425 -0
- package/core/vocabulary/vocabulary-utils.js +704 -0
- package/crates/wasm-router/pkg/package.json +13 -0
- package/crates/wasm-router/pkg/query_router_wasm.d.ts +36 -0
- package/crates/wasm-router/pkg/query_router_wasm.js +271 -0
- package/crates/wasm-router/pkg/query_router_wasm_bg.wasm +0 -0
- package/crates/wasm-router/pkg/query_router_wasm_bg.wasm.d.ts +19 -0
- package/mcp/config-gen.js +121 -0
- package/mcp/server.js +335 -0
- package/mcp/tool-handlers.js +476 -0
- package/package.json +131 -9
- package/scripts/benchmark-harness.js +794 -0
- package/scripts/init.js +1058 -0
- package/scripts/smoke-test.js +435 -0
- package/scripts/uninstall.js +478 -0
- package/scripts/verify-runtime.js +176 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "CoreML variant cascade shape set — SINGLE SOURCE OF TRUTH. Consumed by (1) core/infrastructure/coreml-cascade.js for state inspection, cache resolution, and HF fetch, (2) scripts/spike-coreml/trace_cascade.py for local builds, and (3) the Rust filename parser in crates/sweet-search-native/src/inference/{embedding_model,li_model}.rs (parse_embed_variant_filename, parse_li_variant_filename) via the filePattern convention. Changing shapes here requires re-running scripts/build-coreml-cascade.js to regenerate the .mlpackage artifacts AND re-uploading tarballs to `hfRepo` with fresh checksums. The publish-coreml-cascade helper in the build script automates the tarball+upload+checksum backfill loop.",
|
|
3
|
+
"version": 2,
|
|
4
|
+
"description": "Cascade of fixed-shape NomicBERT embedding and ModernBERT LI mlpackages covering the stair-step output of the JS bucketer in core/embedding/embedding-local-model.js + core/indexing/indexer-pool.js. Distributed as per-variant .tar.gz archives from `hfRepo` so `sweet-search init` can fetch them alongside the other models without requiring Python/coremltools on the end-user machine.",
|
|
5
|
+
"hfRepo": "mrsladoje/sweet-search-coreml-cascade",
|
|
6
|
+
"embed": {
|
|
7
|
+
"modelKey": "nomic-bert",
|
|
8
|
+
"filePattern": "nomic_bert_b{batch}_s{seq}_fp16.mlpackage",
|
|
9
|
+
"tarballPattern": "embed/nomic_bert_b{batch}_s{seq}_fp16.mlpackage.tar.gz",
|
|
10
|
+
"hiddenDim": 768,
|
|
11
|
+
"traceSource": {
|
|
12
|
+
"hfId": "nomic-ai/CodeRankEmbed",
|
|
13
|
+
"safetensors": "model.safetensors",
|
|
14
|
+
"config": "config.json"
|
|
15
|
+
},
|
|
16
|
+
"variants": [
|
|
17
|
+
{ "batch": 64, "seq": 96, "rationale": "hardCap × short seq — covers observed (64, 52..107) regime", "tarballSha256": "76eeb863ffe11cc9d21cd0be0bd47d932630a86480c8a4f019d3360990b8ee14", "tarballSizeBytes": 252549277 },
|
|
18
|
+
{ "batch": 64, "seq": 192, "rationale": "hardCap × short-med", "tarballSha256": "138ae2b406ecc0c2ce87730157791667f007472745079298c9eb605dc4b7ec1c", "tarballSizeBytes": 252608808 },
|
|
19
|
+
{ "batch": 32, "seq": 384, "rationale": "tokenBudget regime", "tarballSha256": "580c67c9b9d3ac85e2c3a76b2262ad0b0c0489889dddfa3bc8cf1f215e71643a", "tarballSizeBytes": 252627736 },
|
|
20
|
+
{ "batch": 16, "seq": 512, "rationale": "long, cache-bound start", "tarballSha256": "6e8dfc0b83befc905f2e90df45ae21d232d7983956c8c1526176fb175e774fbe", "tarballSizeBytes": 252608341 },
|
|
21
|
+
{ "batch": 4, "seq": 1024, "rationale": "long tail", "tarballSha256": "62afa26642d5531fc227ef730cd372312331631be43281464c9c1fb2c59b3f2f", "tarballSizeBytes": 252626657 },
|
|
22
|
+
{ "batch": 1, "seq": 2048, "rationale": "extreme tail / oversize docs", "tarballSha256": "4e3073e231a03d3c7fca673af81ac1fa9e6d28c4ff8da7856d64183eff090532", "tarballSizeBytes": 252722456 }
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"li": {
|
|
26
|
+
"modelKey": "modernbert-li",
|
|
27
|
+
"filePattern": "li_modernbert_b{batch}_s{seq}_fp16.mlpackage",
|
|
28
|
+
"tarballPattern": "li/li_modernbert_b{batch}_s{seq}_fp16.mlpackage.tar.gz",
|
|
29
|
+
"tokenDim": 128,
|
|
30
|
+
"backboneDim": 768,
|
|
31
|
+
"traceSource": {
|
|
32
|
+
"hfId": "lightonai/LateOn-Code",
|
|
33
|
+
"backbone": "model.safetensors",
|
|
34
|
+
"projection": "1_Dense/model.safetensors",
|
|
35
|
+
"config": "config.json"
|
|
36
|
+
},
|
|
37
|
+
"variants": [
|
|
38
|
+
{ "batch": 128, "seq": 48, "rationale": "upperCap × very short — covers observed (128, 18..33)", "tarballSha256": "01dddc23d4a83be98977b0500d9fcf977fb7649bb548bb9977c6d6395f22e1c2", "tarballSizeBytes": 275367487 },
|
|
39
|
+
{ "batch": 128, "seq": 128, "rationale": "upperCap × short", "tarballSha256": "f81af66aef693c4c1994b560d23f1b0fbf97c38c66e0fadda0774af358b9b1a1", "tarballSizeBytes": 275382654 },
|
|
40
|
+
{ "batch": 64, "seq": 256, "rationale": "medium", "tarballSha256": "f5958100ab4a81e724f37338b5ad45c396a5c649402552191632a79d4ef826a9", "tarballSizeBytes": 275405899 },
|
|
41
|
+
{ "batch": 16, "seq": 512, "rationale": "long, cache-bound start", "tarballSha256": "37906ffe3cc9205306cfb50d0b5821e569fd6ee3ec13e4d71f481f1e53649031", "tarballSizeBytes": 275451379 },
|
|
42
|
+
{ "batch": 4, "seq": 1024, "rationale": "long tail", "tarballSha256": "e618f8bd981d24e4984c61ee1788c02578aaf601efc16cd926885a2930833d40", "tarballSizeBytes": 275545042 },
|
|
43
|
+
{ "batch": 1, "seq": 2048, "rationale": "LI max length", "tarballSha256": "d5262bf49f145cadbdbddb82b7934499fc310f5c654c5aae34cfb9c2d90487d2", "tarballSizeBytes": 275743803 }
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// core/coreml-provider.js
|
|
2
|
+
// CoreML execution provider detection and configuration for macOS Apple Silicon.
|
|
3
|
+
|
|
4
|
+
import os from 'os';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check if running on macOS Apple Silicon (ARM64).
|
|
8
|
+
* CoreML EP only benefits Apple Silicon; on Intel Macs it falls back to CPU anyway.
|
|
9
|
+
*/
|
|
10
|
+
export function isAppleSilicon() {
|
|
11
|
+
return process.platform === 'darwin' && os.arch() === 'arm64';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let _coremlAvailable = null;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Check if CoreML EP is available in the onnxruntime-node binary.
|
|
18
|
+
* Uses ort.listSupportedBackends() which is available in ORT >= 1.18.
|
|
19
|
+
* Result is cached after first call.
|
|
20
|
+
*/
|
|
21
|
+
export async function isCoreMLProviderAvailable() {
|
|
22
|
+
if (_coremlAvailable !== null) return _coremlAvailable;
|
|
23
|
+
try {
|
|
24
|
+
const ort = await import('onnxruntime-node');
|
|
25
|
+
if (typeof ort.listSupportedBackends === 'function') {
|
|
26
|
+
const backends = ort.listSupportedBackends();
|
|
27
|
+
_coremlAvailable = backends.some(b => b.name === 'coreml' && b.bundled);
|
|
28
|
+
return _coremlAvailable;
|
|
29
|
+
}
|
|
30
|
+
} catch { /* ignore — ORT not installed or too old */ }
|
|
31
|
+
_coremlAvailable = false;
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Determine if CoreML EP should be used, following the same tri-state pattern
|
|
37
|
+
* as SWEET_SEARCH_USE_OPENVINO: auto | on | off.
|
|
38
|
+
*
|
|
39
|
+
* "auto" (default): enable on Apple Silicon when provider is available.
|
|
40
|
+
* "on"/"1"/"true": force enable (still gated to darwin+arm64).
|
|
41
|
+
* "off"/"0"/"false": force disable.
|
|
42
|
+
*
|
|
43
|
+
* @param {boolean} providerAvailable - result of isCoreMLProviderAvailable()
|
|
44
|
+
*/
|
|
45
|
+
export function shouldUseCoreML(providerAvailable) {
|
|
46
|
+
const raw = (process.env.SWEET_SEARCH_USE_COREML ?? '').trim().toLowerCase();
|
|
47
|
+
if (raw === '0' || raw === 'false' || raw === 'off') return false;
|
|
48
|
+
if (!isAppleSilicon()) return false;
|
|
49
|
+
|
|
50
|
+
const autoMode = raw === '' || raw === 'auto';
|
|
51
|
+
const explicitOn = raw === '1' || raw === 'true' || raw === 'on';
|
|
52
|
+
if (!autoMode && !explicitOn) return false;
|
|
53
|
+
|
|
54
|
+
return providerAvailable;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build the executionProviders array for CoreML with CPU fallback.
|
|
59
|
+
*
|
|
60
|
+
* Two CoreML model formats exist:
|
|
61
|
+
* - MLProgram (coreMlFlags 0x010): newer, supports ANE, requires Core ML 5+.
|
|
62
|
+
* Better performance when supported, but some models fail to compile.
|
|
63
|
+
* - NeuralNetwork (coreMlFlags 0): older default, broader op coverage,
|
|
64
|
+
* silently casts FP32→FP16. Works with more model architectures.
|
|
65
|
+
*
|
|
66
|
+
* ORT 1.24+ uses lowercase backend names ('cpu', 'coreml', 'webgpu').
|
|
67
|
+
* The legacy PascalCase names ('CPUExecutionProvider') are NOT recognized.
|
|
68
|
+
*
|
|
69
|
+
* @param {boolean} useMLProgram - true for MLProgram format, false for NeuralNetwork
|
|
70
|
+
*/
|
|
71
|
+
export function getCoreMLExecutionProviders(useMLProgram = true) {
|
|
72
|
+
return [
|
|
73
|
+
{ name: 'coreml', coreMlFlags: useMLProgram ? 0x010 : 0 },
|
|
74
|
+
'cpu',
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Reset cached state (for testing and A/B benchmark pass switching). */
|
|
79
|
+
export function _resetCoreMLCache() {
|
|
80
|
+
_coremlAvailable = null;
|
|
81
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Utilities - Shared SQLite configuration helpers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import Database from 'better-sqlite3';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Apply read-path PRAGMA optimizations to a read-only database connection.
|
|
9
|
+
*
|
|
10
|
+
* - mmap_size = 256MB: Maps entire DB into OS page cache (typical DBs are 5-50MB).
|
|
11
|
+
* Subsequent reads bypass SQLite's page cache via xFetch().
|
|
12
|
+
* - cache_size = 20MB: Modest bump from default ~16MB. Partially redundant with mmap
|
|
13
|
+
* but acts as safety net for internal bookkeeping.
|
|
14
|
+
* - temp_store = MEMORY (optional): Use only for bounded, latency-sensitive read paths.
|
|
15
|
+
*
|
|
16
|
+
* These are best-effort optimizations. PRAGMA failures should not change the caller's
|
|
17
|
+
* fallback behavior or make a readable database look unavailable.
|
|
18
|
+
*
|
|
19
|
+
* @param {import('better-sqlite3').Database} db
|
|
20
|
+
* @param {{ tempStoreMemory?: boolean }} [options]
|
|
21
|
+
*/
|
|
22
|
+
export function applyReadPragmas(db, options = {}) {
|
|
23
|
+
if (!db || typeof db.pragma !== 'function') return;
|
|
24
|
+
const { tempStoreMemory = false } = options;
|
|
25
|
+
|
|
26
|
+
for (const pragma of [
|
|
27
|
+
'mmap_size = 268435456', // 256MB
|
|
28
|
+
'cache_size = -20000', // 20MB
|
|
29
|
+
]) {
|
|
30
|
+
try {
|
|
31
|
+
db.pragma(pragma);
|
|
32
|
+
} catch {
|
|
33
|
+
// Best-effort only.
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (tempStoreMemory) {
|
|
38
|
+
try {
|
|
39
|
+
db.pragma('temp_store = MEMORY');
|
|
40
|
+
} catch {
|
|
41
|
+
// Best-effort only.
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Warm the graph database page cache with lightweight queries.
|
|
48
|
+
* Opens an ephemeral connection, touches FTS5/relationship/summary pages, closes.
|
|
49
|
+
* Intentionally minimal — no heavy imports, no query infrastructure.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} dbPath - Path to code_graph.db
|
|
52
|
+
* @returns {{ summaries: number }}
|
|
53
|
+
*/
|
|
54
|
+
export function warmGraphDbCache(dbPath) {
|
|
55
|
+
const db = new Database(dbPath, { readonly: true, timeout: 5000 });
|
|
56
|
+
applyReadPragmas(db);
|
|
57
|
+
try {
|
|
58
|
+
try { db.prepare('SELECT count(*) FROM entities_fts WHERE name MATCH "warmup"').get(); } catch { /* table may not exist */ }
|
|
59
|
+
try { db.prepare('SELECT count(*) FROM relationships').get(); } catch { /* table may not exist */ }
|
|
60
|
+
let summaries = 0;
|
|
61
|
+
try {
|
|
62
|
+
const row = db.prepare('SELECT count(*) as cnt FROM entities WHERE summary IS NOT NULL').get();
|
|
63
|
+
summaries = row?.cnt || 0;
|
|
64
|
+
} catch { /* column may not exist */ }
|
|
65
|
+
return { summaries };
|
|
66
|
+
} finally {
|
|
67
|
+
db.close();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dedup hashing — thin JS wrapper over the Rust NAPI addon's
|
|
3
|
+
* SimHash + MinHash + LSH clustering entrypoints.
|
|
4
|
+
*
|
|
5
|
+
* Zero domain logic lives here. If the native addon is missing
|
|
6
|
+
* (unsupported platform), `isAvailable()` returns false and callers
|
|
7
|
+
* fall back to a no-op that treats every chunk as its own exemplar.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createRequire } from 'module';
|
|
11
|
+
import { resolveNativeAddon } from './native-resolver.js';
|
|
12
|
+
import { DEDUP_CONFIG } from './config/dedup.js';
|
|
13
|
+
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
|
|
16
|
+
let _addon = null;
|
|
17
|
+
let _loadAttempted = false;
|
|
18
|
+
let _loadError = null;
|
|
19
|
+
|
|
20
|
+
function loadAddon() {
|
|
21
|
+
if (_loadAttempted) return _addon;
|
|
22
|
+
_loadAttempted = true;
|
|
23
|
+
const addonPath = resolveNativeAddon();
|
|
24
|
+
if (!addonPath) {
|
|
25
|
+
_loadError = new Error('dedup-hashing: native addon not resolved for this platform');
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
_addon = require(addonPath);
|
|
30
|
+
} catch (e) {
|
|
31
|
+
_loadError = e;
|
|
32
|
+
_addon = null;
|
|
33
|
+
}
|
|
34
|
+
return _addon;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Returns true iff the Rust NAPI dedup functions are callable on this machine.
|
|
39
|
+
* Used by callers to decide whether to run the dedup phase or skip it cleanly.
|
|
40
|
+
*/
|
|
41
|
+
export function isAvailable() {
|
|
42
|
+
const addon = loadAddon();
|
|
43
|
+
return !!(
|
|
44
|
+
addon &&
|
|
45
|
+
typeof addon.dedupFingerprintBatch === 'function' &&
|
|
46
|
+
typeof addon.dedupCluster === 'function'
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getLoadError() {
|
|
51
|
+
loadAddon();
|
|
52
|
+
return _loadError;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Compute per-chunk SimHash + MinHash fingerprints for a batch of texts.
|
|
57
|
+
* Returns `[{simhashHex: string, minhash: Buffer(512)}]`, one per input.
|
|
58
|
+
* Throws if the native addon isn't available — callers should check `isAvailable()` first.
|
|
59
|
+
*/
|
|
60
|
+
export function computeFingerprints(texts, config = DEDUP_CONFIG) {
|
|
61
|
+
const addon = loadAddon();
|
|
62
|
+
if (!addon) throw _loadError ?? new Error('dedup native addon unavailable');
|
|
63
|
+
return addon.dedupFingerprintBatch(texts, config.ngramSize, config.numPerm, config.seed);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Cluster fingerprints into near-duplicate groups via banded LSH +
|
|
68
|
+
* SimHash Hamming secondary filter + union-find collapse.
|
|
69
|
+
* Returns `[{exemplarIdx, siblingIdxs, clusterId}]`. Entries include
|
|
70
|
+
* singletons (siblingIdxs.length === 0) so callers can enumerate every chunk's
|
|
71
|
+
* cluster assignment — filter them out to get only true clusters.
|
|
72
|
+
*/
|
|
73
|
+
export function clusterFingerprints(fingerprints, config = DEDUP_CONFIG) {
|
|
74
|
+
const addon = loadAddon();
|
|
75
|
+
if (!addon) throw _loadError ?? new Error('dedup native addon unavailable');
|
|
76
|
+
return addon.dedupCluster(
|
|
77
|
+
fingerprints,
|
|
78
|
+
config.numPerm,
|
|
79
|
+
config.numBands,
|
|
80
|
+
config.jaccardThreshold,
|
|
81
|
+
config.simhashHammingMax,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hardware Capability Detection — identifies the current machine's chip
|
|
3
|
+
* family, accelerator generation, and preferred native inference backend.
|
|
4
|
+
*
|
|
5
|
+
* Used by:
|
|
6
|
+
* - coreml-cascade.js → gates cascade fetch/build on M3+ ANE viability
|
|
7
|
+
* - native-inference.js → backend selection diagnostics
|
|
8
|
+
* - scripts/init.js → decides what artifacts to fetch for the profile
|
|
9
|
+
* - scripts/uninstall.js → only removes cascade cache if it exists
|
|
10
|
+
*
|
|
11
|
+
* Design notes:
|
|
12
|
+
* - `sysctl` is a cheap (~5 ms) one-shot call. The result is cached so
|
|
13
|
+
* repeated consumers (init, native-inference, uninstall) all share one
|
|
14
|
+
* detection. Hardware doesn't change at runtime.
|
|
15
|
+
* - Never throws. Unknown hardware degrades to "candle-cpu fallback" —
|
|
16
|
+
* this module is only advisory; absence of a capability is never an
|
|
17
|
+
* error.
|
|
18
|
+
* - Unknown new Apple chips (e.g. an M5 shipped after this file) are
|
|
19
|
+
* admitted as cascade-eligible via the ">= 3" rule — we prefer
|
|
20
|
+
* optimistic new-hardware behavior to silently refusing to try.
|
|
21
|
+
* - CUDA detection combines two signals: (1) an "addon built with the
|
|
22
|
+
* cuda feature?" probe via `native_cuda_available()`, and
|
|
23
|
+
* (2) `nvidia-smi` output for GPU name + compute capability + VRAM.
|
|
24
|
+
* The addon probe is authoritative for "is CUDA end-to-end usable";
|
|
25
|
+
* `nvidia-smi` fills in the diagnostic fields. See the CUDA Backend
|
|
26
|
+
* section in docs/INIT_STRATEGY.md for the full contract.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { execFileSync } from 'node:child_process';
|
|
30
|
+
import os from 'node:os';
|
|
31
|
+
import { resolveNativeAddon } from './native-resolver.js';
|
|
32
|
+
import { createRequire } from 'node:module';
|
|
33
|
+
|
|
34
|
+
let _cached = null;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parse an Apple chip brand string from `sysctl -n machdep.cpu.brand_string`
|
|
38
|
+
* into a structured descriptor.
|
|
39
|
+
*
|
|
40
|
+
* Known formats:
|
|
41
|
+
* "Apple M1" → { family: "M1", generation: 1, variant: "base" }
|
|
42
|
+
* "Apple M2 Pro" → { family: "M2", generation: 2, variant: "pro" }
|
|
43
|
+
* "Apple M3 Max" → { family: "M3", generation: 3, variant: "max" }
|
|
44
|
+
* "Apple M4 Ultra" → { family: "M4", generation: 4, variant: "ultra" }
|
|
45
|
+
*
|
|
46
|
+
* Returns `null` for unrecognised strings (including Intel brand strings).
|
|
47
|
+
*/
|
|
48
|
+
export function parseAppleChipBrandString(raw) {
|
|
49
|
+
if (!raw || typeof raw !== 'string') return null;
|
|
50
|
+
const match = raw.match(/\bApple\s+(M(\d+))(?:\s+(Pro|Max|Ultra))?\b/i);
|
|
51
|
+
if (!match) return null;
|
|
52
|
+
return {
|
|
53
|
+
family: match[1].toUpperCase(),
|
|
54
|
+
generation: parseInt(match[2], 10),
|
|
55
|
+
variant: match[3] ? match[3].toLowerCase() : 'base',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Read CPU brand string via `sysctl`. Only called on darwin; returns
|
|
61
|
+
* `null` on any failure (sandboxed env, sysctl missing, non-zero exit).
|
|
62
|
+
*/
|
|
63
|
+
function sysctlBrandString() {
|
|
64
|
+
try {
|
|
65
|
+
const out = execFileSync('sysctl', ['-n', 'machdep.cpu.brand_string'], {
|
|
66
|
+
encoding: 'utf-8',
|
|
67
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
68
|
+
timeout: 2000,
|
|
69
|
+
});
|
|
70
|
+
return out.trim() || null;
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Minimum Apple generation for which the CoreML variant cascade is
|
|
78
|
+
* expected to beat candle Metal BF16 at our NomicBERT + ModernBERT
|
|
79
|
+
* workload.
|
|
80
|
+
*
|
|
81
|
+
* Why M3:
|
|
82
|
+
* - M1 ANE ≈ 11 TOPS int8; M2 ≈ 15.8; M3 ≈ 18; M4 ≈ 38.
|
|
83
|
+
* - Our end-to-end measurement on M3 Max saw 18% wall-clock reduction
|
|
84
|
+
* vs Metal BF16 baseline (commit 4fd9c9a).
|
|
85
|
+
* - M1/M2 ANE TOPS are below M3 and the spike's measured per-batch
|
|
86
|
+
* latency improvement on smaller shapes does not cover the mlmodelc
|
|
87
|
+
* compile overhead for those generations. Rather than ship a
|
|
88
|
+
* fancier feature that regresses on older hardware, we gate on M3+.
|
|
89
|
+
* - A future measurement on M1/M2 with a smaller cascade subset could
|
|
90
|
+
* lower this threshold. For now, conservative gating.
|
|
91
|
+
*/
|
|
92
|
+
const MIN_APPLE_GENERATION_FOR_CASCADE = 3;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Minimum NVIDIA compute capability (as a float) we target for the CUDA
|
|
96
|
+
* backend.
|
|
97
|
+
*
|
|
98
|
+
* Why 7.0:
|
|
99
|
+
* - SM 6.x (Pascal, e.g. GTX 1080) has no tensor cores. Candle's
|
|
100
|
+
* stock matmul on CUDA cores runs slower than modern CPU BLAS for
|
|
101
|
+
* our small (b≤64, s≤1024) shapes. Net regression vs CPU.
|
|
102
|
+
* - SM 7.0 (Volta, V100) has first-gen tensor cores + F16 matmul with
|
|
103
|
+
* F32 accumulate. Works, but we pin F32 dtype here because F16 has
|
|
104
|
+
* precision issues on this generation.
|
|
105
|
+
* - SM 7.5 (Turing, T4/RTX 20xx) has second-gen tensor cores. F16
|
|
106
|
+
* works cleanly with F32 accumulate via cuBLASLt / cuDNN.
|
|
107
|
+
* - SM 8.0+ (Ampere/Ada/Hopper) adds BF16 tensor cores. BF16 is the
|
|
108
|
+
* default on this tier, matching the Metal story.
|
|
109
|
+
*
|
|
110
|
+
* See the per-CC dtype policy in
|
|
111
|
+
* crates/sweet-search-native/src/inference/mod.rs::optimal_dtype for
|
|
112
|
+
* the full mapping. JS layer is responsible for parsing the
|
|
113
|
+
* `nvidia-smi` compute_cap output and setting
|
|
114
|
+
* SWEET_SEARCH_CUDA_COMPUTE_CAP before the addon loads models.
|
|
115
|
+
*/
|
|
116
|
+
const MIN_CUDA_COMPUTE_CAPABILITY = 7.0;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Parse `nvidia-smi --query-gpu=name,compute_cap,memory.total,driver_version`
|
|
120
|
+
* CSV output into a structured descriptor. Returns `null` on anything
|
|
121
|
+
* unparseable — the surrounding `detectNvidiaGpu()` treats that as
|
|
122
|
+
* "no GPU" just like a missing `nvidia-smi` binary.
|
|
123
|
+
*
|
|
124
|
+
* Expected first data line:
|
|
125
|
+
* "NVIDIA GeForce RTX 3090, 8.6, 24576, 535.129.03"
|
|
126
|
+
*/
|
|
127
|
+
export function parseNvidiaSmiOutput(raw) {
|
|
128
|
+
if (!raw || typeof raw !== 'string') return null;
|
|
129
|
+
const firstLine = raw.split(/\r?\n/).find((l) => l.trim().length > 0);
|
|
130
|
+
if (!firstLine) return null;
|
|
131
|
+
const cols = firstLine.split(',').map((s) => s.trim());
|
|
132
|
+
if (cols.length < 4) return null;
|
|
133
|
+
const [name, computeCapability, memoryMB, driverVersion] = cols;
|
|
134
|
+
const memInt = parseInt(memoryMB, 10);
|
|
135
|
+
const ccFloat = parseFloat(computeCapability);
|
|
136
|
+
if (!name || !Number.isFinite(ccFloat) || !Number.isFinite(memInt)) return null;
|
|
137
|
+
return {
|
|
138
|
+
name,
|
|
139
|
+
computeCapability, // string "8.6" — kept as string for faithful passthrough
|
|
140
|
+
computeCapabilityFloat: ccFloat, // float for comparisons
|
|
141
|
+
memoryMB: memInt,
|
|
142
|
+
driverVersion,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Shell out to `nvidia-smi` on Linux and return the first GPU's metadata.
|
|
148
|
+
* Silent failure (returns `null`) on any of: non-Linux host, missing
|
|
149
|
+
* binary, non-zero exit, malformed output. Bounded at 2s timeout so a
|
|
150
|
+
* hung GPU doesn't block init.
|
|
151
|
+
*/
|
|
152
|
+
function detectNvidiaGpu() {
|
|
153
|
+
if (process.platform !== 'linux') return null;
|
|
154
|
+
try {
|
|
155
|
+
const out = execFileSync(
|
|
156
|
+
'nvidia-smi',
|
|
157
|
+
[
|
|
158
|
+
'--query-gpu=name,compute_cap,memory.total,driver_version',
|
|
159
|
+
'--format=csv,noheader,nounits',
|
|
160
|
+
],
|
|
161
|
+
{
|
|
162
|
+
encoding: 'utf-8',
|
|
163
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
164
|
+
timeout: 2000,
|
|
165
|
+
},
|
|
166
|
+
);
|
|
167
|
+
return parseNvidiaSmiOutput(out);
|
|
168
|
+
} catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Probe the native Rust addon for CUDA readiness. Returns the boolean
|
|
175
|
+
* reported by `native_cuda_available()` or `null` when the addon is
|
|
176
|
+
* missing / doesn't expose that export (older builds).
|
|
177
|
+
*
|
|
178
|
+
* The addon may return `Some(false)` when the `cuda` feature IS built in
|
|
179
|
+
* but the runtime driver is missing — in that case we know the package
|
|
180
|
+
* is the `-cuda` variant but libcuda.so didn't load, which is worth
|
|
181
|
+
* surfacing to the user.
|
|
182
|
+
*/
|
|
183
|
+
function probeAddonCudaAvailability() {
|
|
184
|
+
try {
|
|
185
|
+
const addonPath = resolveNativeAddon();
|
|
186
|
+
if (!addonPath) return null;
|
|
187
|
+
const addonRequire = createRequire(import.meta.url);
|
|
188
|
+
const addon = addonRequire(addonPath);
|
|
189
|
+
if (typeof addon?.nativeCudaAvailable !== 'function') return null;
|
|
190
|
+
return Boolean(addon.nativeCudaAvailable());
|
|
191
|
+
} catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Detect the current hardware capability. Returns a stable, read-only
|
|
198
|
+
* descriptor that callers use to pick inference backends and decide
|
|
199
|
+
* which artifacts to fetch at init time.
|
|
200
|
+
*
|
|
201
|
+
* Fields:
|
|
202
|
+
* platform — process.platform
|
|
203
|
+
* arch — process.arch
|
|
204
|
+
* totalMemGB — os.totalmem() in GiB (float)
|
|
205
|
+
* logicalCores — os.cpus().length
|
|
206
|
+
* brandString — raw sysctl output (darwin-arm64 only)
|
|
207
|
+
* appleSilicon — parsed chip descriptor or null
|
|
208
|
+
* coremlCascadeEligible — boolean; M3+ darwin-arm64 only
|
|
209
|
+
* coremlCascadeReason — human string explaining eligible/not
|
|
210
|
+
* nvidiaGpu — { name, computeCapability, computeCapabilityFloat,
|
|
211
|
+
* memoryMB, driverVersion } | null
|
|
212
|
+
* cudaAddonEnabled — boolean; addon built with cuda feature + libcuda.so loaded
|
|
213
|
+
* cudaAvailable — boolean; cudaAddonEnabled AND nvidiaGpu AND CC ≥ 7.0
|
|
214
|
+
* cudaReason — human string explaining eligible/not
|
|
215
|
+
* candleGpuBackend — "metal" | "cuda" | null
|
|
216
|
+
* inferenceBackendPreference — "coreml-cascade" | "candle-metal"
|
|
217
|
+
* | "candle-cuda" | "candle-cpu"
|
|
218
|
+
*/
|
|
219
|
+
export function detectHardwareCapability() {
|
|
220
|
+
if (_cached) return _cached;
|
|
221
|
+
|
|
222
|
+
const platform = process.platform;
|
|
223
|
+
const arch = process.arch;
|
|
224
|
+
const totalMemGB = os.totalmem() / (1024 ** 3);
|
|
225
|
+
const logicalCores = os.cpus().length;
|
|
226
|
+
|
|
227
|
+
let brandString = null;
|
|
228
|
+
let appleSilicon = null;
|
|
229
|
+
if (platform === 'darwin' && arch === 'arm64') {
|
|
230
|
+
brandString = sysctlBrandString();
|
|
231
|
+
appleSilicon = parseAppleChipBrandString(brandString);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let coremlCascadeEligible = false;
|
|
235
|
+
let coremlCascadeReason;
|
|
236
|
+
if (platform !== 'darwin') {
|
|
237
|
+
coremlCascadeReason = `CoreML is macOS-only (current platform: ${platform})`;
|
|
238
|
+
} else if (arch !== 'arm64') {
|
|
239
|
+
coremlCascadeReason = `CoreML cascade requires Apple Silicon (current arch: ${arch})`;
|
|
240
|
+
} else if (!appleSilicon) {
|
|
241
|
+
coremlCascadeReason = `Could not identify Apple Silicon chip (sysctl brand: ${brandString || 'unavailable'})`;
|
|
242
|
+
} else if (appleSilicon.generation < MIN_APPLE_GENERATION_FOR_CASCADE) {
|
|
243
|
+
coremlCascadeReason = `${brandString}: ${appleSilicon.family} ANE below cascade threshold (M${MIN_APPLE_GENERATION_FOR_CASCADE}+ required)`;
|
|
244
|
+
} else {
|
|
245
|
+
coremlCascadeEligible = true;
|
|
246
|
+
coremlCascadeReason = `${brandString}: ${appleSilicon.family} ANE suitable for cascade`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// NVIDIA GPU detection — Linux only. `nvidia-smi` tells us what's
|
|
250
|
+
// installed; the addon probe tells us whether the installed native
|
|
251
|
+
// package actually knows how to dispatch to it.
|
|
252
|
+
const nvidiaGpu = detectNvidiaGpu();
|
|
253
|
+
const addonCudaProbe = probeAddonCudaAvailability();
|
|
254
|
+
const cudaAddonEnabled = addonCudaProbe === true;
|
|
255
|
+
|
|
256
|
+
// Diagnostic opt-out: SWEET_SEARCH_CUDA=0 force-disables CUDA even on
|
|
257
|
+
// an otherwise-eligible host. Parallels SWEET_SEARCH_COREML_CASCADE=0.
|
|
258
|
+
const cudaForceDisabled = ['0', 'false', 'off'].includes(
|
|
259
|
+
(process.env.SWEET_SEARCH_CUDA ?? '').toLowerCase(),
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
let cudaAvailable = false;
|
|
263
|
+
let cudaReason;
|
|
264
|
+
if (cudaForceDisabled) {
|
|
265
|
+
cudaReason = 'CUDA force-disabled via SWEET_SEARCH_CUDA=0';
|
|
266
|
+
} else if (platform !== 'linux') {
|
|
267
|
+
cudaReason = `CUDA native backend ships for Linux only (current platform: ${platform})`;
|
|
268
|
+
} else if (!nvidiaGpu) {
|
|
269
|
+
cudaReason = 'No NVIDIA GPU detected (nvidia-smi missing or no device 0)';
|
|
270
|
+
} else if (nvidiaGpu.computeCapabilityFloat < MIN_CUDA_COMPUTE_CAPABILITY) {
|
|
271
|
+
cudaReason = `${nvidiaGpu.name}: compute capability ${nvidiaGpu.computeCapability} below threshold (${MIN_CUDA_COMPUTE_CAPABILITY}+ required)`;
|
|
272
|
+
} else if (addonCudaProbe === null) {
|
|
273
|
+
cudaReason = `${nvidiaGpu.name} detected, but the installed native package does not expose a CUDA probe — reinstall with @sweet-search/native-linux-${arch === 'arm64' ? 'arm64' : 'x64'}-gnu-cuda to enable GPU indexing`;
|
|
274
|
+
} else if (addonCudaProbe === false) {
|
|
275
|
+
cudaReason = `${nvidiaGpu.name} detected, but the native addon reports Device::new_cuda(0) failed (driver / libcuda.so mismatch?)`;
|
|
276
|
+
} else {
|
|
277
|
+
cudaAvailable = true;
|
|
278
|
+
cudaReason = `${nvidiaGpu.name} (compute ${nvidiaGpu.computeCapability}, ${nvidiaGpu.memoryMB} MB, driver ${nvidiaGpu.driverVersion}) — suitable for candle-cuda`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Candle GPU backend availability.
|
|
282
|
+
// darwin-arm64 → metal (bundled with the darwin-arm64 native package)
|
|
283
|
+
// linux-*-gnu + NVIDIA + cuda-enabled addon → cuda
|
|
284
|
+
// everything else → null (falls through to candle CPU)
|
|
285
|
+
let candleGpuBackend = null;
|
|
286
|
+
if (platform === 'darwin' && arch === 'arm64') {
|
|
287
|
+
candleGpuBackend = 'metal';
|
|
288
|
+
} else if (cudaAvailable) {
|
|
289
|
+
candleGpuBackend = 'cuda';
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Preference order: coreml-cascade > candle-metal > candle-cuda > candle-cpu.
|
|
293
|
+
// coreml-cascade and candle-cuda never co-exist on the same host
|
|
294
|
+
// (one is darwin, the other is linux), so the ordering is orthogonal
|
|
295
|
+
// in practice.
|
|
296
|
+
let inferenceBackendPreference;
|
|
297
|
+
if (coremlCascadeEligible) {
|
|
298
|
+
inferenceBackendPreference = 'coreml-cascade';
|
|
299
|
+
} else if (candleGpuBackend === 'metal') {
|
|
300
|
+
inferenceBackendPreference = 'candle-metal';
|
|
301
|
+
} else if (candleGpuBackend === 'cuda') {
|
|
302
|
+
inferenceBackendPreference = 'candle-cuda';
|
|
303
|
+
} else {
|
|
304
|
+
inferenceBackendPreference = 'candle-cpu';
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
_cached = Object.freeze({
|
|
308
|
+
platform,
|
|
309
|
+
arch,
|
|
310
|
+
totalMemGB,
|
|
311
|
+
logicalCores,
|
|
312
|
+
brandString,
|
|
313
|
+
appleSilicon,
|
|
314
|
+
coremlCascadeEligible,
|
|
315
|
+
coremlCascadeReason,
|
|
316
|
+
nvidiaGpu,
|
|
317
|
+
cudaAddonEnabled,
|
|
318
|
+
cudaAvailable,
|
|
319
|
+
cudaReason,
|
|
320
|
+
candleGpuBackend,
|
|
321
|
+
inferenceBackendPreference,
|
|
322
|
+
});
|
|
323
|
+
return _cached;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Reset the cached detection result. Tests only — production callers
|
|
328
|
+
* should never need this because hardware doesn't change at runtime.
|
|
329
|
+
*/
|
|
330
|
+
export function _resetHardwareCapabilityCache() {
|
|
331
|
+
_cached = null;
|
|
332
|
+
}
|