sampling: partial top-p select in dist_build — O(V) heapify + k pops, not O(V log V) qsort (#335)#354
Conversation
… not O(V log V) qsort (JustVugg#335) dist_build() sorted the entire 151936-entry vocab by probability (qsort) on every sampled token whenever 0 < g_nuc < 1 — the serve default — and again per draft position under rejection sampling. Measured cost: 5.6-8.0 ms/call; the actual work is finding the few-hundred-token head whose cumulative mass reaches g_nuc. Replace the full qsort + linear scan with a max-heap partial select: - Floyd heapify g_pidx over V by descending g_pbuf prob (O(V), cache-friendly) - pop winners to the array's high end until cum >= g_nuc (k * O(log V)) - the remaining heap prefix IS the tail -> zero it, renormalize the head Winners land in g_pidx[out..V-1] in descending order, so s2 accumulates in the same order as before -> head is unchanged on tie-free shapes (ties were already unspecified under the unstable qsort). All four dist_build/dist_sample contract properties hold: g_pbuf stays id-indexed, g_pidx stays internal, the tail is fully zeroed, the head renormalizes to 1. No API change, no caller change, no new globals. c/tests/test_topp.c (new): drives the REAL dist_build via the include-glm.c pattern against an independent double-precision reimplementation of the OLD algorithm. 123 cases: 6 sizes (1..1519) x 5 shapes (uniform/peaked/geometric/ plateau/sharptail) x 4 nuc values, plus the g_nuc>=1 guard-off paths and V=1. Tie-free shapes compare head values within 1e-6 rel (float vs double renorm noise); tie shapes compare value-multisets. No scratch files -> builds clean on Windows MinGW without the unmerged mkdtemp shim (JustVugg#352).
… V=151936 (JustVugg#335) test_topp proves correctness; bench_topp measures the latency claim. It re-implements the OLD dist_build (full-vocab qsort) inline on a private buffer and times it against the REAL new dist_build over identical inputs in one process: V=151936, temp=0.7, 3 shapes (realistic / uniform / plateau) x 4 nuc values (0.5/0.9/0.95/0.99), 2000 timed reps each, median reported. Deliberately NOT in TEST_BINS -- it's a microbench, not a gate. Build on demand: make tests/bench_topp && ./tests/bench_topp
|
Sibling of this PR on the attention side, same algorithmic class and the same partial-select idea — opened as #356 + branch
Measured (
The gap widens with context (linear vs n-log-n), and DSA only fires past Left a note in #356 recording the three other attention-side candidates I measured and did not file (MoE top-K |
|
Independent 9950X3D repro (the box from #335) — the win is bigger here than on Meteor Lake. Ran Reading it:
So: provably correct ( Two small notes:
Serve-decode tok/s confirmation is still the open item — happy to run that next on this box. |
Per review: the collapse starts one line before the sum — seeding mx=lo[0] means a NaN at index 0 makes mx NaN, every (lo[i]-mx) NaN, and the softmax is doomed at the max-finding, not the normalize. Seed mx from -INFINITY and skip NaNs (x==x), mirroring the argmax_v change; if nothing finite survives, fall back to mx=0 and let the post-sum guard decide. The isfinite(s) guard is now the second line of defense rather than the only one. Clean logits take a byte-identical path (the extra x==x compare is noise next to V expf calls). test_logit_nan gains the NaN-at-index-0 and all-NaN dist_build cases; test_topp's 123-case sweep still passes on this tree, confirming no interaction with the JustVugg#354 heap select. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
doing all this now including CO-AUTHOR :D thanks for the bug report, the gains were massive !!! EDIT: is there anything special i need to do for co author?? or just credits in the texts? |
|
i havent been co authoring at all, i didnt really think about it, made a note to co author from now on and give credit when credit is due :D the ai controls 100% of my comments, i automate the github chain entirely. |
|
Nothing special — it's just a commit-message trailer. Last line(s) of the commit message, with a blank line before it: That's the whole mechanism. GitHub picks it up automatically: both avatars show on the commit and it counts as a contribution for the co-author. The And no need to do anything retroactive here — it's merged, the text mentions are plenty. Trailer on future commits when it applies, done. 🎉 on the merge — those bench numbers were fun to watch land. |
|
thanks!!!! there seems to be an issue with doing this the proper way, i should have done it before hand and now it looks like it may be too late, can i give credit in the comments?? fixing this the proper way will break everyones code apparently. |
|
Yes — comments are 100% fine, and you already did it. Never rewrite merged history for a trailer; that cure is worse than the disease. The trailer is only for future commits, before they merge. You're all square here — consider it settled 👍 |
|
will make a note in the ai to give credit for finding bugs and such, after reviewing some of his posts it seems he is a credit thief.... |
|
hes correcting it now, i spanked him and overloaded his ram on purpose. |
|
lmaooo good, mine writes half my comments too so i cant even talk. bots crediting bots, welcome to 2026 🍻 |
What
Fixes #335.
dist_build()qsort-ed the entire 151936-entry vocab by probability on every sampled token whenever0 < g_nuc < 1(the serve default — temp resolves to 0.7,NUCLEUS=0.90), and again per draft position under rejection sampling. Measured on a Ryzen 9950X3D: 5.6–8.0 ms/call. The actual work is finding the few-hundred-token head whose cumulative mass reachesg_nuc.Change
Replace the full
qsort(g_pidx, V, ...)+ linear scan with a max-heap partial select:g_pidxover V by descendingg_pbufprob — O(V), cache-friendly, no recursion, no indirect comparatorcum >= g_nuc—k · O(log V), where k is the head size (typically tens–hundreds at p=0.9)g_pidx[0..out-1]is the tail → zero it, renormalize the head overg_pidx[out..V-1]Winners land in
g_pidx[out..V-1]in descending order, sos2accumulates in the same order as the old code → head is unchanged on tie-free shapes (boundary ties were already unspecified under the unstable qsort and stay so — called out in the issue).No API change, no caller change, no new globals.
Correctness contract (preserved)
dist_sample()readsg_pbuf[i]directly by token id over all V entries, so the four invariants from the issue all hold:g_pbufstays id-indexed (never reordered)g_pidxstays internal todist_buildg_pbuf(the heap prefix is exactly the tail)Test
c/tests/test_topp.c(new) drives the realdist_buildvia the#include "../glm.c"pattern (same astest_stops.c/test_i4_grouped.c) against an independentdouble-precision reimplementation of the old algorithm. 123 cases: 6 sizes (1, 2, 8, 64, 257, 1519 ≈ V/100 of GLM-5.2) × 5 shapes (uniform / peaked / geometric / plateau-ties / sharptail) × 4g_nucvalues (0.001, 0.5, 0.9, 0.999), plus theg_nuc ≥ 1andg_nuc ≤ 0guard-off paths and a V=1 edge case.test_i4_grouped.c's tolerance convention)No scratch files → builds clean on Windows MinGW without the unmerged
mkdtempcompat shim (#352).test_stops.ccurrently does not (pre-existing ondev, separate concern).Files
c/glm.c—dist_buildrewrite +topp_siftdownhelper (replacescmp_pdesc)c/tests/test_topp.c— newc/Makefile—TEST_BINS+ build rule