engram: batch-cosine Adapter/Strategy/Factory over ggml, supersedes hand-rolled PR #114
El SDK CI - dev / build-and-test (pull_request) Failing after 4m29s
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).
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
/* 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);
|
||||
}
|
||||
Reference in New Issue
Block a user