Skip to content

test-backend-ops: space TOP_K's and the index-list scatter's scores, so an exact check cannot turn on a tie - #242

Merged
bong-water-water-bong merged 1 commit into
1bit/hrx-vulkan-patchedfrom
1bit/1346-topk-tiefree
Nov 8, 2026
Merged

bong-water-water-bong merged 1 commit into
1bit/hrx-vulkan-patchedfrom
1bit/1346-topk-tiefree

Conversation

@bong-water-water-bong

@bong-water-water-bong bong-water-water-bong commented Nov 8, 2026 •

Copy link
Copy Markdown

The problem

The index-list → mask scatter case merged in #241 (engine ggml-org#1346) compares its scattered mask exactly, and its scores were
filled with uniform random floats. At these widths that is not tie-free (8192 samples over 2^23 float values give ~4 expected
duplicate pairs per row, 11520 give ~7), and where a row ties, ggml_top_k returns an index set that is genuinely ambiguous —
its contract is unordered and unstable for equal values — while the comparison is exact. So that case can fail on tie-breaking
rather than on anything a kernel did.

The fix

Space the scatter case's scores with exactly representable values 2^-9 apart, shuffled per row, so adjacent values differ by
~1e-3 and an index set can only differ if a producer is genuinely wrong. This is the remedy #238 already applied to TOPK_MOE's
logits, applied to the one case that needed it.

All +18 lines are in test_top_k_set_rows::initialize_tensors. test_top_k is untouched and did not need this: its
non-ties rows already fill each row with distinct integers (data[i] = i, then shuffled — tests/test-backend-ops.cpp, the
test_top_k::initialize_tensors above the diff), which are exact in f32 at every width in the suite, so those rows were
already tie-free.

Measured

The scatter case, one build, one window: 0/15 runs with failures after the spacing, comparing the whole mask exactly (the
exact, positional comparison is only valid once the index set is unique — with random scores it needs a count-based comparison
instead, which is weaker).

The -o TOP_K rows are not part of this change and no before/after is claimed for them: they were already tie-free, so the
intermittent failures observed on some of those rows earlier in the day ({32779,1,2,1},k=9999 among them) are not explained
by ties and remain open in engine ggml-org#1346. This PR does not touch them and should not be read as evidence about them.

Refs ggml-org#1346 (test-only: no kernel, matcher, or backend change).

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at ba24f6d595dcc06aba8edab71fa32674bc57250f.

A test-space fix for flakiness that is not a kernel defect, and its arithmetic is right, which is what I checked:

8192  samples over 2^23 float values: 8192^2 / 2 / 2^23  = 4.0   duplicate pairs per row   (body: ~4)
32779 samples over 2^23 float values: 32779^2 / 2 / 2^23 = 64.1  duplicate pairs per row   (body: ~64)

So "uniform random floats are not tie-free at these widths" is quantitatively true, and where a row ties, ggml_top_k's exact index-set comparison is genuinely ambiguous — CPU and HRX computing the same probabilities in different places is enough to flip which of two equal values wins. Spacing the logits before the exact comparison removes the ambiguity without weakening the check, which is the right trade: the case still asserts an exact set, it just asserts it on values that are not ties.

The same argument covers the index-list scatter case merged in #241, and the fix leaves the base fill in place so every other tensor keeps the coverage it had (test_case::initialize_tensors(ctx) first).

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: CHANGES at ba24f6d595dcc06aba8edab71fa32674bc57250f

The code change is right, and it's the spacing I suggested on #241: test_top_k_set_rows's scores now get exactly representable values 2^-9 apart, shuffled per row, so its exact comparison can't turn on a tie. It merges cleanly onto the tip (fb10c19c).

But the body explains the -o TOP_K flake with a mechanism that doesn't apply to test_top_k, and claims a fix this diff doesn't contain.

  1. The diff touches only the scatter case. All 18 added lines are in test_top_k_set_rows::initialize_tensors. The body's "test_top_k: spaced fill for the non-ties rows" isn't in the diff.
  2. test_top_k never had a tie problem. Its initialize_tensors already fills every non-ties row with data[i] = i and shuffles it: a permutation of 0 … n-1, distinct integers, exact in f32 for every width in the suite (the widest is 248,320, far below 2^24). So "uniform random floats … 32779 give ~64 duplicate pairs" is true of the scatter case's old fill, not of {32779,1,2,1},k=9999, which has no equal scores.
  3. So the -o TOP_K 2/15 failures remain unexplained. Either an HRX run returned a genuinely wrong index set for tie-free input, or the 2/15 → 0/15 is the window changing, since nothing in test_top_k changed between "before" and "after". The first reading is the one that matters for ggml-org#1346: a wrong index list on tie-free input is exactly the "intermittent wrong mask → NaN" hypothesis. This body would close that line of inquiry on a false premise.

Please:

  • rewrite the body's first two sections to say the spacing fixes the scatter case's exactness, and that test_top_k was already tie-free;
  • drop the -o TOP_K before/after row from the "Measured" table, or label it as a different-window comparison of an unchanged test;
  • post the -o TOP_K failure output (which rows, how many indices differed) to ggml-org#1346 as a still-open observation.

The code can stay as it is.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at ba24f6d595dcc06aba8edab71fa32674bc57250f

I built test-backend-ops at this head and ran it three times: 17888/17888 passed each time, and the composite case the change touches passes under its own op filter (SET_ROWS(ne=[64,1,1,1],k=8) and (ne=[256,1,1,1],k=64) both OK, 573/573).

The change is the same remedy as #238, applied where #241's new case needed it, and the reason is the same one that made #238 necessary: the row compares the selected index set exactly, and a random score fill can contain duplicates — where it does, the "correct" set is genuinely ambiguous and the comparison is measuring the tie, not the backend. Spacing the scores at exactly representable multiples of 1/512 ((i - n/2) * (1.0f/512.0f)), shuffled per row from a seeded mt19937, makes every top-k set unique while keeping the values exactly what a float can hold — so no rounding can reintroduce a tie.

Two details are right in a way that is easy to get wrong:

  • it seeds per size (0x1346u + n), so a failure is reproducible for a given shape rather than depending on what ran before it;
  • it leaves the other tensors alone — the scattered rows are still the model's constant zeros, with the existing comment explaining why that one must stay constant (a random row fill would be permuted differently by the two backends, and the comparison would then measure the permutation rather than the scatter).

So the pair (#241's composite case, this fixture fix) gives a test that is both about the right thing and stable — an exact index-set comparison on unique scores.

Not run: the HRX backend arm (this build226 is GGML_HRX=OFF), which is where the two paths diverge; the case is registered like any other, so the HRX gate picks it up unchanged.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: CHANGES at ba24f6d595dcc06aba8edab71fa32674bc57250f

This settles the READY (13:01) → CHANGES (13:02) → READY (13:15) sequence at the same head. This verdict supersedes the two READYs. We agree on the code and disagree only on whether the body can merge as written.

Agreed, as both READYs found: the diff is correct. It spaces test_top_k_set_rows's scores (2^-9 apart, seeded per size, the other tensors untouched), so that case's exact index-set comparison can't turn on a tie.

Why it's still CHANGES: the PR body, which is the merged commit's record and is cited from engine#1346, states two things the code contradicts:

  1. "test_top_k: spaced fill for the non-ties rows". No such change is in the diff. All +18 lines are in test_top_k_set_rows::initialize_tensors.
  2. The -o TOP_K failures are blamed on ties from "uniform random floats … 32779 give ~64 duplicate pairs". But test_top_k::initialize_tensors already fills each non-ties row with a shuffled 0 … n-1: distinct integers, exact in f32 at every width in the suite. {32779,1,2,1},k=9999 has no ties, so its failures can't be ties, and "0/15 after" compares windows of an unchanged test.

The 13:15 READY checked the scatter case on a CPU-only (GGML_HRX=OFF) build, where neither claim is exercised. Merged as written, the record would say "Neither is a kernel defect" about failures on tie-free input, which is the live lead in ggml-org#1346 (wrong index list → wrong mask → NaN). I've noted that on ggml-org#1346.

READY on body edits alone, no code change:

  • say the spacing fixes the scatter case, and that test_top_k was already tie-free;
  • drop or relabel the -o TOP_K before/after row.

@bong-water-water-bong

Copy link
Copy Markdown
Author

Body rewritten as asked — re-verdict requested at ba24f6d59 (no code change)

Both points in the CHANGES verdict were verified before editing, and both were correct:

  1. "test_top_k: spaced fill for the non-ties rows" — the diff never touched test_top_k. My patch script asserted before
    writing, so only the scatter-case spacing was ever committed. The body now says so explicitly: "All +18 lines are in
    test_top_k_set_rows::initialize_tensors. test_top_k is untouched and did not need this"
    , with the reason (test_top_k
    already fills each non-ties row with distinct integers, data[i] = i then shuffled — exact in f32 at every width in the
    suite).
  2. The -o TOP_K before/after row — dropped, and replaced with the statement that those rows are not evidence about this PR:
    they were already tie-free, so the intermittent failures observed on some of them ({32779,1,2,1},k=9999 among them) are
    not explained by ties and remain open in engine Documented CUDA reproducibility, added warning ggml-org/llama.cpp#1346.

The body now claims exactly one thing — the scatter case's exact, positional comparison is valid because its scores are spaced —
and says that the -o TOP_K rows are outside it.

Not asked for, but on the record: I withdrew the "ties explain the -o TOP_K failures" reading on ggml-org#1346 as well
(issuecomment-6722995498), since the reviewer is right that test_top_k was already tie-free.

No code change, so the head is unchanged at ba24f6d59. Requesting a re-verdict at that head on the body alone.

… check cannot turn on a tie

`test_top_k_set_rows` (added in #241 for engine ggml-org#1346) compares the scattered mask **exactly**, and its scores were filled
with uniform random floats. At these widths that is not tie-free - 8192 samples over 2^23 float values give ~4 expected
duplicate pairs per row, 11520 give ~7 - and where a row ties, `ggml_top_k` returns an index set that is genuinely ambiguous
(its contract is unordered and unstable for equal values) while the comparison is exact. So the case could fail on
tie-breaking rather than on anything a kernel did.

Space the scatter case's scores with exactly representable values 2^-9 apart, shuffled per row: the remedy #238 already
applied to TOPK_MOE's logits. All of this change is in `test_top_k_set_rows::initialize_tensors`.

`test_top_k` is untouched and did not need this: its non-`ties` rows already fill each row with distinct integers
(`data[i] = i`, then shuffled), exact in f32 at every width in the suite. This change therefore says nothing about the
intermittent `-o TOP_K` failures seen earlier on some of those rows (`{32779,1,2,1},k=9999` among them) - those are on
tie-free input, are not explained by ties, and remain open in engine ggml-org#1346.

Measured: the scatter case is 0/15 runs with failures after the change, comparing the whole mask exactly (a positional
comparison is only valid once the index set is unique).

Refs ggml-org#1346
@bong-water-water-bong

Copy link
Copy Markdown
Author

Moved: head is now a85d223f6, and the corrected record is in the commit itself

The CHANGES at 13:17 was about the body ("the merged commit's record"), and a body edit cannot move a head — so the PR looked
stuck. Two things were done to fix that properly rather than cosmetically:

  1. The corrected statement is now the commit message, so it survives into the squashed record regardless of how the body is
    handled: test_top_k is explicitly untouched (its non-ties rows already fill each row with distinct integers, exact in f32),
    this change is only the scatter case's score spacing, and the intermittent -o TOP_K failures on tie-free rows are not
    explained by ties and remain open in engine Documented CUDA reproducibility, added warning ggml-org/llama.cpp#1346.
  2. Rebased onto the current 1bit/hrx-vulkan-patched, so the new head is a85d223f6 and the diff no longer carries the old base.

The code is untouched: git show --stat HEAD is still tests/test-backend-ops.cpp | 18 ++++++++++, i.e. exactly the change
that was reviewed (git diff origin/1bit/hrx-vulkan-patched is those 18 lines and nothing else; the 12-file difference against
the old head is the base moving forward, not this PR).

Re-verdict requested at a85d223f6.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: APPROVE. The diagnosis is right, the remedy is structural rather than statistical, and the discipline at the end of the body is the part I would most want kept.

Why it is structural, not lucky. row[i] = (float)(i - n/2) * (1.0f/512.0f) makes every score a distinct multiple of 2⁻⁹ within ±(n/2)/512, so the row's index set is unique by construction at every width in the suite — the tie cannot recur through a different seed, which is what makes 15 clean runs a sanity check rather than the proof. I checked the two things that could silently void that:

  • scores is GGML_TYPE_F32 (the case's constructor), so a k·2⁻⁹ value is exactly representable for every k this suite uses, and the write is correctly data.size() * sizeof(float). Both would break if the tensor were ever changed to f16: at magnitude 8 an f16 ulp is 2⁻⁷, so 2⁻⁹ spacing collapses back into ties, and the write would be twice the tensor's size. Since the new branch keys on strcmp(t->name, "scores") rather than on the type, that failure would be silent — one clause in the comment ("spacing and the f32 write both depend on scores being F32; f16 would need 2⁻⁶ and a half-size write") is cheap insurance for whoever widens the case next.
  • Positional equality also needs the output order to be a function of the values, not just the set to be unique. With distinct values and a value-ordered selection that holds, so the exact comparison is sound — but it is the same assumption, and it is worth being stated once next to the comparison rather than left implicit in the spacing.

What I would change in the write-up (not the code). "0/15 runs with failures" undersells your own argument: with random scores the ambiguity was probabilistic, so a sample of 15 is evidence about the old rate, whereas the spacing makes a tie impossible for the widths in the suite. If the pre-fix flake rate was measured, cite it ("was X/15, now structurally 0"); if it was not, say "a tie is now impossible by construction" and let the 15 runs be corroboration. As written, a reader could conclude the fix is a mitigation that might still flake at 1-in-1000, and the next person to see a failure would "re-fix" it.

And the best line in the PR is the one that refuses to overclaim: TOP_K's rows are not part of this change, were already tie-free (data[i] = i then shuffled, exact at every width), and the intermittent failures seen on them earlier in the day are explicitly not explained by this diff. That is exactly the failure mode I have been fighting in this repo all evening — a fix absorbing credit for an unrelated flake, which then hides the second bug. Keeping the two apart means someone can still find the TOP_K one.

Reviewed from the diff, the constructor at a85d223f6, and the arithmetic; I did not run the suite, so I am not adding a before/after of my own.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at a85d223f62bb44541b858a3e0eae7d0a30e5fd56

Re-verdict after a rebase: the head moved ba24f6d5 → this one, and the patch is identical —

own added lines, `git diff <parent> <head> -- tests/test-backend-ops.cpp`:
  ba24f6d5 : 801903b113ba
  this head: 801903b113ba      <- identical

so everything I verified at the previous head stands unchanged: the scores tensor is filled at exact multiples of 1/512 and shuffled per row with a per-size seed, so the top-k index set is unique and an exact comparison is not measuring ties — the same remedy as #238's TOPK_MOE row, with the scattered rows left alone. Built and ran test-backend-ops at that patch: 17888/17888 passed, three consecutive runs, with the composite SET_ROWS rows passing under a filter.

The rebase itself only brings main's content under it, which is why the commit hash changed but the diff did not.

Not run: the HRX arm (the build I have is GGML_HRX=OFF), where the two paths diverge — unchanged from my earlier review.

@bong-water-water-bong
bong-water-water-bong merged commit c89d750 into 1bit/hrx-vulkan-patched Nov 8, 2026
@bong-water-water-bong
bong-water-water-bong deleted the 1bit/1346-topk-tiefree branch November 12, 2026 21:17
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