Skip to content

sampling: guard against non-finite logits — was silent token-0 spew (#369)#370

Open
KingIcyCreamProjects wants to merge 2 commits into
JustVugg:devfrom
KingIcyCreamProjects:fix/logit-nan-guard
Open

sampling: guard against non-finite logits — was silent token-0 spew (#369)#370
KingIcyCreamProjects wants to merge 2 commits into
JustVugg:devfrom
KingIcyCreamProjects:fix/logit-nan-guard

Conversation

@KingIcyCreamProjects

Copy link
Copy Markdown
Contributor

What

Fixes #369: on the default sampling path (TEMP>0, 0<NUCLEUS<1), a single NaN/+Inf logit poisoned softmax into an all-NaN g_pbuf, after which dist_sample silently emitted token 0 forever — no error, just an unbroken run of one token. The greedy path was equally blind (argmax_v NaN-pinned to index 0).

Three small changes, all O(1)/free on the happy path:

  • argmax_v: skip NaN (x==x) and seed from -INFINITY → returns the max finite/+Inf entry instead of a NaN-pinned 0. Covers greedy decode and the speculative-verify argmax.
  • dist_build max scan (per @woolcoxm's review on sampling: a single NaN/+Inf logit makes dist_build emit token 0 forever, silently (default path) #369): seed mx from -INFINITY and NaN-skip the scan — the collapse actually started at the max-finding (mx=lo[0] with a NaN at 0 dooms every lo[i]-mx before the sum), not at the normalize. If nothing finite survives, fall back to mx=0 and let the post-sum guard decide.
  • dist_build post-sum guard: if s is non-finite or ≤0, collapse g_pbuf to a one-hot over the finite argmax and warn once — degrade + diagnose, never silently corrupt. Now the second line of defense, not the only one.

Clean logits take a byte-identical path.

Placement vs #354

Rebased onto current dev (post-#354/#357). As predicted in review, the #354 heap rewrite only touched the nucleus tail — the guard sits right after the normalize, before the if(g_nuc>0 && g_nuc<1.f) block, so the heap never sees an all-NaN g_pbuf.

Tests

tests/test_logit_nan.c (wired into TEST_BINS), 6 assertion blocks: argmax_v NaN-skip / +Inf-pick / all-NaN; dist_build with a NaN mid-vocab → finite one-hot on the max finite logit and dist_sample emits it (not 0); NaN at index 0 (the mx-seed failure mode from review); all-NaN stays finite; and a clean-logits regression case. Fails on stock dev (first assert: argmax NaN-pinned to 0), passes with this change.

Also re-ran test_topp (123 cases) on this tree: green — no interaction with the #354 partial select.

@woolcoxm — rebased, mx-scan point folded in, ready for your review.

KingIcyCreamProjects and others added 2 commits July 17, 2026 14:37
On the default serve path (TEMP>0, 0<NUCLEUS<1) a single NaN or +Inf in
the logits — a bad streamed expert tile, or an fp overflow in the matmul
at a low-RAM eviction boundary — poisoned softmax: g_pbuf became all-NaN,
dist_sample never satisfied cum>=u, and the fallback returned token 0. The
engine then emitted an unbroken run of token 0 with NO error. The greedy
path was equally blind: argmax_v started bv=lo[0] and `lo[i]>NaN` is always
false, so a NaN at index 0 pinned the argmax to 0.

- argmax_v: skip NaN (x==x) and seed from -inf, so it returns the max
  finite/+Inf entry instead of being NaN-pinned to 0. Covers greedy decode
  and the speculative-verify argmax path.
- dist_build: after the softmax sum, if s is non-finite or <=0, collapse
  g_pbuf to a one-hot over the finite argmax and warn once, instead of
  dividing every entry into NaN. Covers the nucleus and verify paths.

Both are O(1)/free on the happy path (one branch after the existing loop;
one extra comparison inside the existing argmax loop). Degrade + diagnose,
never silently corrupt.

test_logit_nan (wired into TEST_BINS): asserts argmax_v skips NaN/picks
+Inf, dist_build yields a finite normalized one-hot on the max finite
logit, dist_sample emits that token (not 0), and clean logits still give a
valid distribution. Fails on stock dev, passes with this change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant