f853ea8a0b
Rename glm.c → colibri.c and extract four self-contained modules into header-only files (same pattern as st.h/tier.h/grammar.h): quant.h (672 lines) — SIMD matmul kernels, quantization sample.h (143 lines) — RNG, top-p sampling, stop-set kv_persist.h (121 lines) — .coli_kv disk persistence telemetry.h (189 lines) — dashboard protocol, stats, usage Main engine file shrinks from 6588 to 5396 lines (−18%). Build system: primary target is now colibri$(EXE); `make glm` remains as a phony alias for backward compat. CI, setup.sh, coli CLI, and all 10 test files that include the engine are updated. make check passes (C + Python, 73 tests, zero warnings).
224 lines
11 KiB
C
224 lines
11 KiB
C
/* DSA top-keep partial-select: the quickselect rewrite (#356) must produce a
|
|
* BIT-IDENTICAL kept-position set to the old full-vocab qsort, for every score
|
|
* shape the attention indexer can see.
|
|
*
|
|
* Why this test exists (#356): attention_rows() selects the top-`keep` context
|
|
* keys (index_topk=2048 on GLM-5.2) to attend to. It previously did this by
|
|
* full-qsorting all `nk` scores (O(nk log nk)) and reading tmp[keep-1] as the
|
|
* threshold. It now does a partial_select_desc (quickselect, O(nk) average) and
|
|
* takes the threshold as the min of the selected top-keep block. The contract is
|
|
* subtle but STRONGER than test_topp's:
|
|
*
|
|
* The two position-order scans that build dst[] --
|
|
* for t: if isc[t] > thr -> keep (strictly above threshold)
|
|
* for t: if isc[t] == thr -> keep (ties, in position order)
|
|
* -- are UNCHANGED by the rewrite. So if the threshold value is identical,
|
|
* the kept-position set is identical element-by-element (not just as a
|
|
* multiset, which is all the unstable sampling heap in #335 could promise).
|
|
*
|
|
* Strategy: drive the REAL partial_select_desc (via the include-glm.c pattern)
|
|
* and replicate the production threshold derivation + scans, then compare the
|
|
* resulting dst[] against an INDEPENDENT reference that re-implements the OLD
|
|
* algorithm (full qsort + tmp[keep-1] threshold) on a private buffer. The kept
|
|
* sets must be element-wise equal on every shape, including tie plateaus where
|
|
* the boundary membership is decided by the position scan.
|
|
*
|
|
* We also directly unit-test partial_select_desc's partition invariant: after
|
|
* the call, max(a[keep..n)) <= min(a[0..keep)) -- i.e. the keep largest really
|
|
* did land in the prefix. This catches a broken quickselect even before the
|
|
* end-to-end comparison.
|
|
*
|
|
* In-memory only (no scratch files), so it builds clean on the Windows MinGW CI
|
|
* job without the unmerged compat shim. */
|
|
#define main coli_glm_main_unused
|
|
#include "../colibri.c"
|
|
#undef main
|
|
|
|
#include <math.h>
|
|
|
|
static int g_nfails = 0;
|
|
|
|
#define FAIL(fmt, ...) do { \
|
|
fprintf(stderr, " FAIL [%s nk=%d keep=%d shape=%s]: " fmt "\n", \
|
|
label, nk, keep, shape_name, ##__VA_ARGS__); \
|
|
g_nfails++; \
|
|
return; \
|
|
} while (0)
|
|
|
|
/* ---- independent reference: the OLD algorithm (full qsort + tmp[keep-1]) ---- */
|
|
/* qsort comparator matching the production cmp_fdesc exactly (desc, unstable). */
|
|
static int cmp_ref_desc(const void *a, const void *b){
|
|
float x=*(const float*)a, y=*(const float*)b; return x<y?1:x>y?-1:0; }
|
|
|
|
/* Reproduce the OLD glm.c:2589-2596 exactly: copy, qsort desc, threshold =
|
|
* tmp[keep-1], then the two position-order scans into dst[]. Returns nd. */
|
|
static int keep_old(const float *isc, int nk, int keep, int *dst){
|
|
float *tmp=malloc((size_t)nk*sizeof(float));
|
|
memcpy(tmp,isc,(size_t)nk*sizeof(float));
|
|
qsort(tmp,(size_t)nk,sizeof(float),cmp_ref_desc);
|
|
float thr=tmp[keep-1];
|
|
int nd=0;
|
|
for(int t=0;t<nk && nd<keep;t++) if(isc[t]>thr) dst[nd++]=t;
|
|
for(int t=0;t<nk && nd<keep;t++) if(isc[t]==thr) dst[nd++]=t;
|
|
free(tmp);
|
|
return nd;
|
|
}
|
|
|
|
/* Reproduce the NEW glm.c path: partial_select desc, threshold = min of the
|
|
* selected block, same two scans. Uses the REAL partial_select_desc from glm.c. */
|
|
static int keep_new(const float *isc, int nk, int keep, int *dst){
|
|
float *tmp=malloc((size_t)nk*sizeof(float));
|
|
memcpy(tmp,isc,(size_t)nk*sizeof(float));
|
|
partial_select_desc(tmp,nk,keep);
|
|
float thr=tmp[0]; for(int t=1;t<keep;t++) if(tmp[t]<thr) thr=tmp[t];
|
|
int nd=0;
|
|
for(int t=0;t<nk && nd<keep;t++) if(isc[t]>thr) dst[nd++]=t;
|
|
for(int t=0;t<nk && nd<keep;t++) if(isc[t]==thr) dst[nd++]=t;
|
|
free(tmp);
|
|
return nd;
|
|
}
|
|
|
|
/* ---- direct unit test of the partition invariant ---- */
|
|
static void check_partition(const char *label, const float *isc, int nk, int keep,
|
|
const char *shape_name){
|
|
float *tmp=malloc((size_t)nk*sizeof(float));
|
|
memcpy(tmp,isc,(size_t)nk*sizeof(float));
|
|
partial_select_desc(tmp,nk,keep);
|
|
/* invariant: every element of tmp[0..keep) is >= every element of tmp[keep..n).
|
|
* (>=, not >: equal values may sit on either side of the partition boundary,
|
|
* which is fine -- the threshold is the MIN of the prefix, and the position
|
|
* scan handles ties.) */
|
|
float top_min=INFINITY, tail_max=-INFINITY;
|
|
for(int i=0;i<keep;i++) if(tmp[i]<top_min) top_min=tmp[i];
|
|
for(int i=keep;i<nk;i++) if(tmp[i]>tail_max) tail_max=tmp[i];
|
|
if(!(top_min >= tail_max))
|
|
FAIL("partition invariant violated: top_min=%.9g < tail_max=%.9g", top_min, tail_max);
|
|
free(tmp);
|
|
}
|
|
|
|
/* ---- end-to-end: old vs new kept-set must be element-wise identical ---- */
|
|
static void check_case(const char *label, int nk, int keep, const char *shape_name,
|
|
const float *isc){
|
|
int *da=malloc((size_t)nk*sizeof(int));
|
|
int *db=malloc((size_t)nk*sizeof(int));
|
|
int na=keep_old(isc,nk,keep,da);
|
|
int nb=keep_new(isc,nk,keep,db);
|
|
|
|
/* 1. both keep exactly `keep` positions (the contract: keep the top-keep by
|
|
* count). A count mismatch is a real bug, not a tie artifact. */
|
|
if(na!=keep) FAIL("old kept %d, expected %d (old path is the reference)", na, keep);
|
|
if(nb!=keep) FAIL("new kept %d, expected %d", nb, keep);
|
|
if(na!=nb) FAIL("keep-count mismatch: old=%d new=%d", na, nb);
|
|
|
|
/* 2. element-wise identical dst[]. This is the strong contract: because the
|
|
* threshold is derived identically and the position-order scans are byte-
|
|
* for-byte the same, the kept SET and its ORDER must match exactly. (This
|
|
* is what makes #356 cleaner than #335, which was multiset-only.) */
|
|
int first_diff=-1;
|
|
for(int i=0;i<na;i++){ if(da[i]!=db[i]){ first_diff=i; break; } }
|
|
if(first_diff>=0)
|
|
FAIL("kept-set differs at index %d: old dst[%d]=%d new dst[%d]=%d",
|
|
first_diff, first_diff, da[first_diff], first_diff, db[first_diff]);
|
|
|
|
/* 3. also check the partition invariant directly (catches a subtly broken
|
|
* quickselect even if the threshold happened to come out right). */
|
|
check_partition(label,isc,nk,keep,shape_name);
|
|
|
|
free(da); free(db);
|
|
printf(" ok [nk=%d keep=%d shape=%s]\n", nk, keep, shape_name);
|
|
}
|
|
#undef FAIL
|
|
|
|
/* deterministic xorshift32 RNG (matches the test_i4_grouped.c / test_topp.c convention) */
|
|
static uint32_t rng_state = 0x12345678u;
|
|
static uint32_t xr(void){ rng_state ^= rng_state << 13; rng_state ^= rng_state >> 17;
|
|
rng_state ^= rng_state << 5; return rng_state; }
|
|
static double frand(void){ return (xr() >> 8) * (1.0 / 16777216.0); } /* [0,1) */
|
|
|
|
/* fill scores for a given shape. Shapes stress the threshold boundary and the
|
|
* quickselect's median-of-three pivot differently. */
|
|
static void fill_shape(float *isc, int nk, int shape){
|
|
switch(shape){
|
|
case 0: /* uniform random distinct (no ties): the clean contract case */
|
|
for(int i=0;i<nk;i++) isc[i]=(float)(frand()*1000.0); break;
|
|
case 1: /* peaked: a few hot, long distinct tail (realistic attention shape) */
|
|
for(int i=0;i<nk;i++) isc[i]=(float)(-1.0 - frand()*4.0);
|
|
isc[0]=3.f; if(nk>3) isc[nk/3]=1.f; if(nk>2) isc[nk/2]=0.5f; break;
|
|
case 2: /* strictly decreasing geometric (no ties): sorted input -- worst case
|
|
* for a naive quickselect; median-of-three must handle it */
|
|
for(int i=0;i<nk;i++) isc[i]=(float)(-0.001*(double)i); break;
|
|
case 3: /* strictly increasing (reverse-sorted): the other quickselect worst case */
|
|
for(int i=0;i<nk;i++) isc[i]=(float)(0.001*(double)i); break;
|
|
case 4: /* plateau ties: blocks of equal value -> boundary membership decided
|
|
* entirely by the position scan (exercises the ==thr path) */
|
|
for(int i=0;i<nk;i++) isc[i]=(float)(-(double)(i/7)); break;
|
|
case 5: /* all-equal: every value identical -> degenerate threshold, all kept
|
|
* via the ==thr scan; quickselect must not infinite-loop or corrupt */
|
|
for(int i=0;i<nk;i++) isc[i]=5.f; break;
|
|
}
|
|
}
|
|
|
|
int main(void){
|
|
/* Sizes around the real index_topk=2048 boundary, plus small cases that
|
|
* exercise the k>=n / k==1 / k==n edges. */
|
|
int nks[] = {1, 2, 8, 64, 2049, 4097, 8193};
|
|
int keeps[] = {1, 8, 256, 1024, 2048};
|
|
int n_shapes = 6;
|
|
|
|
int cases = 0;
|
|
for(size_t ni=0; ni<sizeof(nks)/sizeof(nks[0]); ni++){
|
|
int nk=nks[ni];
|
|
float *isc=malloc((size_t)nk*sizeof(float));
|
|
for(int shape=0; shape<n_shapes; shape++){
|
|
/* skip shapes that write out of bounds on tiny nk (fill_shape guards
|
|
* the hot-spots with nk>n, but skip the plateau/geometric edge if nk<7) */
|
|
fill_shape(isc,nk,shape);
|
|
for(size_t ki=0; ki<sizeof(keeps)/sizeof(keeps[0]); ki++){
|
|
int keep=keeps[ki];
|
|
if(keep>nk) continue; /* keep<=nk invariant of the production code */
|
|
if(keep<=0) continue;
|
|
char label[40]; snprintf(label,sizeof(label),"nk[%zu]/keep[%zu]/shape[%d]",ni,ki,shape);
|
|
const char *sn=(const char*[]){"random","peaked","decreasing","increasing","plateau","all-equal"}[shape];
|
|
check_case(label,nk,keep,sn,isc);
|
|
cases++;
|
|
}
|
|
}
|
|
free(isc);
|
|
}
|
|
|
|
/* edge: keep == nk (nothing to partition; both paths keep everything) */
|
|
{
|
|
int nk=100, keep=100; float isc[100];
|
|
for(int i=0;i<nk;i++) isc[i]=(float)frand();
|
|
int *db=malloc(sizeof(int)*nk); int nb=keep_new(isc,nk,keep,db);
|
|
if(nb!=nk){ fprintf(stderr," FAIL [keep==nk]: kept %d expected %d\n",nb,nk); g_nfails++; }
|
|
else printf(" ok [keep==nk nk=%d]\n",nk);
|
|
free(db); cases++;
|
|
}
|
|
/* edge: keep == 1 (threshold = the single max; quickselect must find it) */
|
|
{
|
|
int nk=500, keep=1; float isc[500];
|
|
for(int i=0;i<nk;i++) isc[i]=(float)frand();
|
|
int da[1],db[1]; int na=keep_old(isc,nk,keep,da), nb=keep_new(isc,nk,keep,db);
|
|
if(na!=1||nb!=1||da[0]!=db[0]){
|
|
fprintf(stderr," FAIL [keep==1]: old={%d (n=%d)} new={%d (n=%d)}\n",da[0],na,db[0],nb); g_nfails++; }
|
|
else printf(" ok [keep==1 argmax=%d]\n",db[0]); cases++;
|
|
}
|
|
/* edge: all-equal scores, keep in the middle -> every kept slot is a tie;
|
|
* the position scan must pick positions 0..keep-1 deterministically */
|
|
{
|
|
int nk=1000, keep=500; float isc[1000];
|
|
for(int i=0;i<nk;i++) isc[i]=3.14f;
|
|
int *db=malloc(sizeof(int)*nk); int nb=keep_new(isc,nk,keep,db);
|
|
int bad=0; for(int i=0;i<nb;i++) if(db[i]!=i) bad=1;
|
|
if(nb!=keep||bad){ fprintf(stderr," FAIL [all-equal keep=%d]: nb=%d bad=%d\n",keep,nb,bad); g_nfails++; }
|
|
else printf(" ok [all-equal keep=%d -> positions 0..%d]\n",keep,keep-1); cases++;
|
|
free(db);
|
|
}
|
|
|
|
printf("\ntest_dsa_select: %d cases run, %d failure(s)\n", cases, g_nfails);
|
|
if(g_nfails){ printf("test_dsa_select: FAIL\n"); return 1; }
|
|
printf("test_dsa_select: ok\n");
|
|
return 0;
|
|
}
|