b3f410fc91
El SDK CI - dev / build-and-test (pull_request) Failing after 4m29s
Stop hand-rolling GPU kernels for batch cosine similarity — use ggml (the MIT-licensed compute library underneath llama.cpp, installed standalone via Homebrew) as the preferred backend, without ripping out PR #114's carefully-verified hand-rolled Metal shader. Structure: one stable public adapter (eg_cosine_batch.h, zero #ifdef at call sites) backed by three selectable concrete Strategies behind an internal vtable (eg_cosine_batch_strategy.h) chosen by a Factory (eg_cosine_batch.c): - eg_cosine_batch_strategy_ggml.c — NEW. ggml + dynamically-loaded Metal backend plugin (ggml_backend_load_all_from_path + ggml_mul_mat for the batched dot product), gather/scatter around the -2.0 sentinel contract. - eg_cosine_batch_strategy_metal_hand.m — PR #114's original hand-rolled Metal shader bridge, preserved almost verbatim, now one strategy among several rather than the only option. eg_cosine_batch.metal kept byte-identical to the original. - eg_cosine_batch_strategy_cpu.c — universal always-false fallback (direct descendant of PR #114's eg_metal_cosine_stub.c). Selection: EL_COSINE_BATCH_STRATEGY=ggml|metal|cpu|auto (default: ggml first, then hand-rolled Metal, then CPU — first available wins), plus back-compat EL_METAL_COSINE=0 to disable every GPU-backed strategy. build_vindex_bench.sh compiles all three strategies on Darwin, CPU-fallback-only elsewhere. vindex_bench.c now reports BRUTE-GGML and BRUTE-METAL side by side against the same CPU oracle, on the same dataset, in one run (real numbers vs. real store snapshot in the PR body).
122 lines
5.1 KiB
C
122 lines
5.1 KiB
C
/* eg_cosine_batch.c — the Factory. Implements the stable public interface
|
|
* declared in eg_cosine_batch.h by selecting ONE concrete
|
|
* EgCosineBatchStrategy (eg_cosine_batch_strategy.h) and dispatching every
|
|
* call to it. This is the ONLY file that branches on EG_HAVE_STRATEGY_*
|
|
* (build-time: which strategy .c/.m files were actually compiled in for
|
|
* this platform) — call sites never see those macros.
|
|
*
|
|
* Selection is lazy (first call) and cached — mirrors the lazy-init caching
|
|
* every individual strategy already does internally, so there is no added
|
|
* per-call cost after the first.
|
|
*
|
|
* Selection mechanism (env var + build-time + runtime capability probe, all
|
|
* three, exactly as directed):
|
|
* - BUILD-TIME decides which strategies exist to choose from at all: a
|
|
* Darwin build compiles+links the ggml strategy and the hand-rolled
|
|
* Metal strategy (EG_HAVE_STRATEGY_GGML / EG_HAVE_STRATEGY_METAL_HAND
|
|
* both defined); a non-Darwin build compiles neither, matching PR #114's
|
|
* original Linux behavior exactly (CPU-fallback only, no Objective-C
|
|
* compiler or Metal frameworks required).
|
|
* - RUNTIME CAPABILITY PROBE: each candidate strategy's own available()
|
|
* does the real, cheap-after-first-call check (device present, backend
|
|
* plugin loaded, pipeline compiled) — never assumed from build-time
|
|
* alone. A build that HAS the ggml strategy compiled in but is running
|
|
* on hardware/software where it can't actually initialize (backend
|
|
* plugin missing, no GPU) correctly falls through to the next candidate.
|
|
* - ENV VAR gives explicit, debuggable override for either axis:
|
|
* EL_COSINE_BATCH_STRATEGY = "ggml" | "metal" | "cpu" | unset/"auto"
|
|
* forces a specific strategy (falling back to cpu if the forced one
|
|
* isn't actually available), or leaves the default auto-preference
|
|
* order in place.
|
|
* EL_METAL_COSINE = 0/n/N/f/F (back-compat with PR #114's vindex_bench
|
|
* gate) disables ALL GPU-backed strategies outright, same as before.
|
|
*
|
|
* DEFAULT preference order when nothing is forced: ggml, then hand-rolled
|
|
* Metal, then CPU fallback — first candidate whose available() reports true
|
|
* wins. This is what makes "stop hand-rolling GPU kernels, use ggml" real
|
|
* rather than nominal: ggml is what actually runs by default on this
|
|
* machine today (see the PR body for the measured numbers backing that).
|
|
*/
|
|
#include "eg_cosine_batch.h"
|
|
#include "eg_cosine_batch_strategy.h"
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
static bool g_selected = false;
|
|
static const EgCosineBatchStrategy* g_active = NULL;
|
|
|
|
static bool eg_env_truthy_off(const char* v) {
|
|
return v && (v[0]=='0' || v[0]=='n' || v[0]=='N' || v[0]=='f' || v[0]=='F');
|
|
}
|
|
|
|
static const EgCosineBatchStrategy* eg_select_strategy(void) {
|
|
if (g_selected) return g_active;
|
|
g_selected = true;
|
|
|
|
const EgCosineBatchStrategy* cpu = eg_cosine_batch_strategy_cpu();
|
|
const char* force = getenv("EL_COSINE_BATCH_STRATEGY");
|
|
const char* legacy_off = getenv("EL_METAL_COSINE");
|
|
|
|
if (eg_env_truthy_off(legacy_off)) { g_active = cpu; return g_active; }
|
|
|
|
if (force && strcmp(force, "cpu") == 0) { g_active = cpu; return g_active; }
|
|
|
|
if (force && strcmp(force, "ggml") == 0) {
|
|
#ifdef EG_HAVE_STRATEGY_GGML
|
|
const EgCosineBatchStrategy* s = eg_cosine_batch_strategy_ggml();
|
|
if (s->available()) { g_active = s; return g_active; }
|
|
#endif
|
|
g_active = cpu; return g_active;
|
|
}
|
|
|
|
if (force && strcmp(force, "metal") == 0) {
|
|
#ifdef EG_HAVE_STRATEGY_METAL_HAND
|
|
const EgCosineBatchStrategy* s = eg_cosine_batch_strategy_metal_hand();
|
|
if (s->available()) { g_active = s; return g_active; }
|
|
#endif
|
|
g_active = cpu; return g_active;
|
|
}
|
|
|
|
/* auto (unset, or any other value): ggml -> metal-hand -> cpu, first
|
|
* available wins. */
|
|
#ifdef EG_HAVE_STRATEGY_GGML
|
|
{
|
|
const EgCosineBatchStrategy* s = eg_cosine_batch_strategy_ggml();
|
|
if (s->available()) { g_active = s; return g_active; }
|
|
}
|
|
#endif
|
|
#ifdef EG_HAVE_STRATEGY_METAL_HAND
|
|
{
|
|
const EgCosineBatchStrategy* s = eg_cosine_batch_strategy_metal_hand();
|
|
if (s->available()) { g_active = s; return g_active; }
|
|
}
|
|
#endif
|
|
g_active = cpu;
|
|
return g_active;
|
|
}
|
|
|
|
bool eg_cosine_batch_available(void) {
|
|
return eg_select_strategy()->available();
|
|
}
|
|
|
|
const char* eg_cosine_batch_strategy_name(void) {
|
|
return eg_select_strategy()->name;
|
|
}
|
|
|
|
bool eg_cosine_batch(const float* query, int32_t qdim,
|
|
const float* const* node_ptrs,
|
|
const int32_t* node_dims,
|
|
int32_t n,
|
|
double* out_scores) {
|
|
return eg_select_strategy()->batch(query, qdim, node_ptrs, node_dims, n, out_scores);
|
|
}
|
|
|
|
bool eg_cosine_batch_multi(const float* queries, int32_t qdim, int32_t nq,
|
|
const float* const* node_ptrs,
|
|
const int32_t* node_dims,
|
|
int32_t n,
|
|
double* out_scores) {
|
|
return eg_select_strategy()->batch_multi(queries, qdim, nq, node_ptrs, node_dims, n, out_scores);
|
|
}
|