Repository navigation
docs(sampling): explain why per-row filter params are keyed by row_idx - #5346
Conversation
|
No actionable comments were generated in the recent review. Walkthrough
Merge Risk:
|
|
The change is complete on this head and |
f714ec9 to
cdf28a8
Compare
## Scope Related to flashinfer-ai#3605 — this PR adds the Sampling side of the quality-fuzzing plan (contract layer, independent CPU oracle, deterministic layer, distribution layer, failure ledger). It does not redo the Norm/RoPE work or the Attention fuzzer, and speculative sampling is out of scope for this first version. In scope: `sampling_from_probs`, `sampling_from_logits`, the individual top-k / top-p / min-p filters, the combined orderings (`top_k_first` vs `joint`), plus two production defects found while building the oracle. Out of scope: any public API change, `chain_speculative_sampling`, CUDA-graph/compile paths. ## Implementation Two production fixes, both about per-row filter parameters when `indices` is used: - `TopKSamplingFromProbKernel` read `top_k_arr[bx]` and `MinPSamplingFromProbKernel` read `min_p_arr[bx]`, while the launcher validates that tensor against `probs.size(0)` (the unique-row count). Whenever `indices` reuses rows (`batch_size > unique_batch_size`) that is an out-of-bounds device read *and* the wrong row's parameter. Both now key on `row_idx`, like the top-p and joint kernels already did. - The Python shims always materialised the per-row `top_k` tensor as `int32`, while the C++ side reads that buffer as the output `IdType`. With int64 `indices` the int32 buffer was read as int64: garbage `k` (filtering silently disabled) plus an OOB read. The buffer is now materialised in the output dtype. The test suite is layered the way the issue plan describes: - **Contract first**, per API, with the source line each fact was read from: retention is value-based (top-k keeps every token equal to the k-th largest value, not k tokens), top-p is strict at the boundary while min-p is inclusive at `max(p) * min_p`, `top_k_first` is *not* `joint`, `valid` differs per API (all-zero rows return `(0, False)` everywhere except `min_p_sampling_from_probs`, whose predicate accepts zeros), and the documented RNG offset advance is `ceil(increment / 4) * 4`. - **Independent CPU oracle** that implements softmax and the filters itself and never calls the code under test; `top_k_first` and `joint` get separate references. - **Deterministic layer**: one-hot / unique-candidate inputs must return the only legal token; the same fixed `seed`+`offset` must reproduce the documented behaviour. - **Distribution layer** with a pre-declared budget: `K = 44` comparisons, `alpha = 0.01`, `N = 200000` trials per case, acceptance `|p_hat - p| <= sqrt(log(2K/alpha) / (2N))` (half width 0.004765). The plan was calibrated against pure reference sampling before being used on the device: 0/30 false positives, power 0.833 at 1.0x half width, 1.000 at 2.0x and at an injected 0.02 bias, 1.000 against a permuted row mapping. - **Failure ledger**: seed/offset, input rule, filter order, thresholds, indices, reference support, observed frequencies and versions, so a failure is reproducible rather than seed-rolled away. ## Validation FlashInfer base: `dc04f50c9aa3eabcdaa5feb0934edb3d85e9529a` Hardware and software: NVIDIA L20 (SM89), driver 595.91.07, CUDA 12.8, torch 2.13.0+cu130, Python 3.10 ``` python -m pytest -q tests/sampling/test_sampling_contract_cpu.py # 12 passed, 0 failed ``` GPU layer status (first live run on the L20, single GPU): ``` CUDA_VISIBLE_DEVICES=<n> FLASHINFER_SAMPLING_FUZZ_SHARD=0/4 \ python -m pytest -q tests/sampling/test_sampling_quality_fuzz.py # 25 failed, 3 skipped ``` Two failure classes, both stated plainly: - the harness itself indexes a CUDA tensor with a CPU index tensor in `singular.repeat(case.calls)[rows]` — a test-side device-placement defect, being fixed; - the first `illegal memory access` occurs in `struct/keying/top_k_int64`, i.e. exactly the int64-indices keying scenario the production fix above addresses, after which the CUDA context poisons the remaining cases. Whether the kernel still reads out of bounds on some path or the harness feeds an invalid input is **not yet attributed**; the next step is a single-case run with `CUDA_LAUNCH_BLOCKING=1` against both the patched and the unpatched tree. This PR is opened as a draft for that reason: the CPU contract/oracle/statistics layer is green and reviewable, the GPU layer is not yet green. ## Results and limitations Correctness: CPU layer 12/12; the two production fixes have regression cases that fail on the base tree. Performance: not evaluated; the suite adds no hot-path work. Memory: not evaluated. Limitations: GPU layer not green yet (above); per-test timeout / `--forked` isolation and the multi-GPU shard launch are not wired here. Signed-off-by: 0z5a <[email protected]>
Signed-off-by: 0z5a <[email protected]>
Signed-off-by: 0z5a <[email protected]>
cdf28a8 to
9123c50
Compare
|
@0z5a thanks for chasing these. Both fixes in this draft have now landed through the parallel sampling slice:
The int64 |
Retain the upstream dtype and row-index fixes, which already include the original implementation, and preserve the per-row parameter notes. Signed-off-by: 0z5a <[email protected]>
|
@0z5a after your 10-21 merge of main, this PR's diff is the 6-line comment in The comment is accurate and worth keeping. If you retitle the PR to something like |
|
@YangXu1990uiuc Done: retitled to |
YangXu1990uiuc
left a comment
There was a problem hiding this comment.
Thanks @0z5a. Approving the comment-only change; I've added run-ci and will merge once the check passes.
kahyunnam
left a comment
There was a problem hiding this comment.
Approving pending CI — the comment documents the right rule for the per-row top_k / min_p read (keyed by the probability row, as in the top-p and joint kernels), and the PR changes no code; thanks for cutting it down to this.
One non-blocking wording suggestion is inline.
The public H100 lane on this head was cancelled rather than failed, so I will start a CI run on this head before merge.
| const uint32_t row_idx = indices == nullptr ? bx : indices[bx]; | ||
| // Per-row parameters are keyed by the probability row they describe, like the top-p and | ||
| // joint kernels. Indexing by bx would read past a length-`unique_batch_size` tensor whenever | ||
| // `indices` reuses rows, i.e. whenever batch_size > unique_batch_size. |
There was a problem hiding this comment.
Small precision point: reusing rows is not the same as batch_size > unique_batch_size (e.g. indices=[0,0,2] with 3 rows reuses row 0 but bx stays in bounds and just picks the wrong row's threshold), and only the latter overflows. Could you collapse this to one accurate line here and at :1137, e.g. // One entry per probs row: index by row_idx, not bx (#5340).?
|
@flashinfer-bot run tests/test_env.py |
Signed-off-by: 0z5a <[email protected]>
Head branch was pushed to by a user without write access
|
@flashinfer-bot run tests/test_env.py |
|
@flashinfer-bot run tests/cli/test_cli_cmds.py |
Description
Adds a comment to
TopKSamplingFromProbKernelandMinPSamplingFromProbKernelininclude/flashinfer/sampling.cuh. It explains why the per-rowtop_k/min_pvalues are read atrow_idxand not atbx: the per-row tensor has one entry per probability row. Indexing bybxcan select the wrong row's threshold whenindicesreorders or reuses rows, and can read past the tensor when the output batch exceeds the number of probability rows.No code change. The fixes themselves landed in #5340 and #5348, and the fuzzer this PR originally carried was superseded by #5341. After merging main into this branch, the diff is only this comment, so the title and description have been cut down to match.
Related: #3605.
Summary by CodeRabbit