/* Microbenchmark: old (full-vocab qsort) vs new (heap partial-select) top-p truncation. * * This is NOT a unit test -- test_topp.c proves correctness. This measures the headline * claim of #335: that replacing the O(V log V) qsort over all 151936 vocab entries with * an O(V) heapify + k*log-V pops is materially faster per call, which is the win the issue * was opened for. * * It re-implements the OLD dist_build inline (qsort + scan) on a private buffer so the A/B * runs in one process, same inputs, same warm caches -- a controlled comparison. It calls * the REAL (new) dist_build via the include-glm.c pattern on the global g_pbuf. * * Methodology (chosen to be honest, not to flatter the change): * - V = 151936 (the actual GLM-5.2 vocab), g_nuc swept across the values that matter * for serving: 0.5 / 0.9 (serve default) / 0.95 / 0.99. * - Three logit shapes: (a) realistic peaked -- one hot token, long exponential tail, * the shape real language-model logits take; (b) uniform -- worst case for the heap, * maximum pop count; (c) a plateau of ties, to exercise the tie path. * - Each (shape, nuc) is timed over N_REPEAT=2000 iterations, with the RNG/logits frozen * so both algorithms do IDENTICAL work. We report median ns/call and the new/old ratio. * - A warmup pass primes caches before timing. * * Run: make tests/bench_topp && ./tests/bench_topp (not in TEST_BINS -- not a gate) */ #define main coli_glm_main_unused #include "../glm.c" #undef main #include #include /* ---- the OLD algorithm, verbatim from dev before #354, on a private buffer ---- */ static float *s_pbuf; static int *s_pidx; static double *s_ref; static int cmp_pdesc_old(const void *a, const void *b){ double pa = s_ref[*(const int*)a], pb = s_ref[*(const int*)b]; return pa < pb ? 1 : pa > pb ? -1 : 0; } static void dist_build_old(const float *lo, int V, double temp, double nuc){ double mx = lo[0]; for (int i = 1; i < V; i++) if (lo[i] > mx) mx = lo[i]; double s = 0, invt = 1.0 / (temp > 1e-4 ? temp : 1e-4); for (int i = 0; i < V; i++){ s_ref[i] = exp((lo[i]-mx)*invt); s += s_ref[i]; } for (int i = 0; i < V; i++) s_ref[i] /= s; if (nuc > 0 && nuc < 1.0){ for (int i = 0; i < V; i++) s_pidx[i] = i; qsort(s_pidx, V, sizeof(int), cmp_pdesc_old); double cum = 0; int keep = V; for (int i = 0; i < V; i++){ cum += s_ref[s_pidx[i]]; if (cum >= nuc){ keep = i+1; break; } } double s2 = 0; for (int i = keep; i < V; i++) s_ref[s_pidx[i]] = 0; for (int i = 0; i < keep; i++) s2 += s_ref[s_pidx[i]]; for (int i = 0; i < keep; i++) s_ref[s_pidx[i]] /= s2; } (void)s_pbuf; } /* ---- timing: median of N_REPEAT runs in ns/call, sorted ascending ---- */ #define N_REPEAT 2000 static double bench_ns(void (*fn)(const float*,int,double,double), const float *lo, int V, double temp, double nuc){ static double ts[N_REPEAT]; for (int r = 0; r < N_REPEAT; r++){ double t0 = now_s(); fn(lo, V, temp, nuc); ts[r] = (now_s() - t0) * 1e9; } /* insertion sort the N_REPEAT samples (small), take median */ for (int a = 1; a < N_REPEAT; a++){ double k = ts[a]; int b = a-1; while (b >= 0 && ts[b] > k){ ts[b+1] = ts[b]; b--; } ts[b+1] = k; } return ts[N_REPEAT/2]; } /* the NEW algorithm is the real dist_build, but it writes g_pbuf (not a private buf). * Wrap it so the bench signature matches, and set the globals it reads. */ static void dist_build_new(const float *lo, int V, double temp, double nuc){ g_temp = (float)temp; g_nuc = (float)nuc; dist_build(lo, V); } /* deterministic logit fill for three shapes */ static uint32_t brng = 0xA5A5A5A5u; static double brand(void){ brng ^= brng << 13; brng ^= brng >> 17; brng ^= brng << 5; return (double)(brng >> 8) * (1.0 / 16777216.0); } static void fill_realistic(float *lo, int V){ /* one hot, exponential tail -- like real logits */ for (int i = 0; i < V; i++) lo[i] = (float)(-4.0 * brand() - (double)i * 0.0001); lo[0] = 6.f; lo[V/50] = 4.f; lo[V/200] = 3.f; } static void fill_uniform(float *lo, int V){ /* worst case for the heap: max pop count */ for (int i = 0; i < V; i++) lo[i] = 0.f; } static void fill_plateau(float *lo, int V){ /* ties: blocks of equal value */ for (int i = 0; i < V; i++) lo[i] = (float)(-(double)(i / 50)); } int main(void){ int V = 151936; float *lo = malloc((size_t)V * sizeof(float)); s_ref = malloc((size_t)V * sizeof(double)); s_pidx = malloc((size_t)V * sizeof(int)); /* force the new dist_build to allocate g_pbuf/g_pidx at full V once */ g_temp = 0.7f; g_nuc = 0.9f; dist_build(lo, V); double temp = 0.7; struct { const char *name; void (*fill)(float*,int); } shapes[] = { { "realistic", fill_realistic }, { "uniform", fill_uniform }, { "plateau", fill_plateau }, }; double nucs[] = { 0.5, 0.9, 0.95, 0.99 }; printf("bench_topp: top-p truncation, old (qsort) vs new (heap) V=%d temp=%.2f\n", V, temp); printf("%-12s %6s %14s %14s %9s %9s\n", "shape", "nuc", "old ns/call", "new ns/call", "speedup", "keep"); printf("-----------------------------------------------------------------------------\n"); for (size_t sh = 0; sh < sizeof(shapes)/sizeof(shapes[0]); sh++){ shapes[sh].fill(lo, V); for (size_t ni = 0; ni < sizeof(nucs)/sizeof(nucs[0]); ni++){ double nuc = nucs[ni]; /* warmup both paths so caches/branch predictors are primed */ for (int w = 0; w < 50; w++){ dist_build_old(lo, V, temp, nuc); dist_build_new(lo, V, temp, nuc); } double t_old = bench_ns(dist_build_old, lo, V, temp, nuc); double t_new = bench_ns(dist_build_new, lo, V, temp, nuc); /* keep count = non-zero entries the new path leaves (== old's keep) */ int keep = 0; for (int i = 0; i < V; i++) if (g_pbuf[i] != 0.f) keep++; printf("%-12s %6.2f %14.0f %14.0f %8.2fx %9d\n", shapes[sh].name, nuc, t_old, t_new, t_old / t_new, keep); } printf("\n"); } printf("bench_topp: done (lower ns is better; speedup = old/new)\n"); free(lo); free(s_ref); free(s_pidx); return 0; }