Skip to content

feat(examples): exercise and audit top-k/top-p sampling end to end - #4216

Draft
aleozlx wants to merge 15 commits into
flashinfer-ai:mainfrom
aleozlx:llm-e2e-sampling
Draft

aleozlx wants to merge 15 commits into
flashinfer-ai:mainfrom
aleozlx:llm-e2e-sampling

Conversation

@aleozlx

@aleozlx aleozlx commented Jul 28, 2026

Copy link
Copy Markdown
Member

📌 Description

Closes a structural hole in the e2e LLM example harness (#4083, which this
stacks on — the diff includes #4083's commits until it lands).

Every stage of the harness ran greedy: --temperature defaults to 0.0 and
sample_tokens() short-circuits to logits.argmax(), so
flashinfer.sampling.top_k_top_p_sampling_from_logits was never invoked
and the sampling and topk JIT modules were never even compiled.
Measured on B200: the greedy path builds 4 modules, the sampling path builds
2 more. Nothing in the suite had ever touched them.

Running the kernel is not enough — a wrong answer has to be caught. So
--check-sampling audits every sampled token against a plain-torch reference
support (top-k ∩ nucleus, from the same temperature-scaled logits). Both
boundaries close over ties and the eps slack is applied on the permissive
side only, so the mask is a superset of what any correct implementation may
keep and a violation is unambiguous. This mirrors how
tests/utils/test_sampling.py asserts membership.

Membership alone has two blind spots, both covered:

  • a vacuous check — with top_k=0/top_p=1.0 the admissible set is the
    whole vocabulary and can never fail. So the mean admissible set size is
    reported and bounded. Measured: 4.3 of 151936, i.e. genuinely selective.
  • a sampler degenerated to argmax — always inside the support, so
    membership is blind to it. So divergences from greedy are counted against
    how many the filtered distribution predicts. Measured: 15 observed vs
    16.58 expected
    .

Also audited: ids in range, seeded replay on identical logits, and the same
membership check with per-request tensor top_k/top_p — a different
kernel path, since a scalar top_k on a large vocab only ever exercises the
top_k_first fast path.

smoke_test.py gains a sampling run pair alongside the greedy pair, asserting
all of the above plus seeded cross-process determinism and sampling/topk
JIT cache reuse. Greedy remains the default everywhere and its existing
assertions are untouched
; --skip-sampling restores the old behavior.

🔍 Related Issues

Stacks on #4083. Relevant to #4189 (__launch_bounds__ gated to SM107): that
change makes B200 sampling codegen match what main already ships (main has
zero launch_bounds in sampling.cuh), so it is not a new Blackwell path —
but this is the first e2e coverage of sampling either way.

🧪 Tests

Validated on B200 (sm100a) against flashinfer 0.6.16rc3 (the tree that
carries #4189's gate — verified present in the tested checkout):

Check Result
sample_violations / 72 draws 0
sample_out_of_range 0
sample_allowed_mean 4.3 of 151936 (bar ≤ 2·top_k)
divergences vs expected 15 vs 16.58
sample_replay_match 1
sample_perreq_violations 0
smoke_test.py (4 runs) PASS — run 3 built topk+sampling, run 4 built 0
reference_check.py --arch dense unregressed

Note on the cross-process sampled-determinism bar: it is a hard assertion
rather than gated on a logits fingerprint, because the harness already
asserts greedy determinism as a hard bar. If model numerics ever stopped being
bit-reproducible, greedy determinism would fail first and more legibly.

✅ Checklist

  • Passes pre-commit run -a
  • Validated on real hardware (B200)
  • Greedy default path unchanged
  • Sampling under TEP/torchrun (non-zero ranks send stdout to devnull, so a
    violation there surfaces only as a nonzero exit code)

AI-assisted (Claude Code).

aleozlx and others added 15 commits July 21, 2026 21:16
…e test

Add examples/pytorch/llm/: a self-contained Llama/Qwen2/Qwen3 dense decoder
built from FlashInfer serving-path ops (BatchPrefill/BatchDecode paged-KV
wrappers, append_paged_kv_cache, RoPE, rmsnorm/fused_add_rmsnorm,
silu_and_mul, top-k/top-p sampling), loading weights directly from HF
safetensors with no inference framework in the dependency chain.

smoke_test.py verifies integration-level invariants unit tests cannot see:
JIT cache reuse across processes (zero rebuilds on a warm run), zero
steady-state recompiles during decode, greedy determinism, and liveness.

Tracking doc: docs/design_docs/e2e_pytorch_llm_examples.md (motivation,
B200 model-fit analysis, roadmap for MoE / quantized / TP phases).

AI-assisted (Claude Code).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…uild() calls

JitSpecNvcc.try_load() returns None for JIT-path modules by design (ninja
owns freshness), so build() runs every process and no-ops on a warm cache.
Count a compile only when the built artifact changes; report raw build()
invocations separately as jit_build_calls. Record first-deployment findings
in the design doc.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… v5 compat)

apply_chat_template's tokenized return type changed across transformers
versions; render to text and tokenize explicitly instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rving

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eference check

Add qwen3_moe to the example model: HF expert weights stacked into the
cutlass_fused_moe w31/w2 layout (up_proj above gate_proj; SiLU applied
in-kernel), router softmax->topk->renorm in fp32 mirroring the HF
Qwen3MoeSparseMoeBlock, per-layer sparse/dense selection.

reference_check.py builds tiny random-weight dense/MoE checkpoints with
transformers and asserts our last-token prefill logits match transformers
eager within a relative-L2 tolerance — same kernel paths as Qwen3-30B-A3B
without the 60GB download. GenerationEngine gains prefill_logits() for this.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, num_local_experts)

v5 nests rope under config.rope_parameters and serializes the MoE expert
count as num_local_experts. The old flat schema read rope_theta=1e4 by
default for v5-saved checkpoints, skewing RoPE frequencies — caught by
reference_check.py as a with-length-growing logits divergence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Shard weights at load: attention QKV column-parallel / o_proj row-parallel,
dense FFN gate/up column- / down row-parallel, MoE experts expert-parallel
via cutlass_fused_moe ep_size/ep_rank with global routing on every rank.
Two allreduces per layer (post-attention, post-FFN/MoE) over NCCL. Launch
with torchrun; reference_check.py compares the TEP result on rank 0 against
the single-GPU transformers reference.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Measures import/load/cold-prefill (JIT warmup)/warm prefill/decode
throughput and steady-state recompile count, single-GPU or TEP via
torchrun; JSON output for future performance gates. Autotune re-measure
behind --autotune. CUDA-graph decode capture is a noted follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ops note

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closes a structural hole in the harness: every stage ran greedy
(`--temperature 0.0` short-circuits to `argmax`), so
`flashinfer.sampling.top_k_top_p_sampling_from_logits` was never invoked and
the `sampling` and `topk` JIT modules were never even compiled. Measured on
B200: greedy builds 4 modules, sampling builds 2 more — nothing in the suite
had ever touched them.

Running the kernel is not enough; a wrong answer has to be caught. So
`--check-sampling` audits every sampled token against a plain-torch reference
support (top-k ∩ nucleus over the same temperature-scaled logits). Both
boundaries close over ties and the eps slack is applied on the permissive side
only, so the mask is a superset of what any correct implementation may keep
and a violation is unambiguous. Mirrors how tests/utils/test_sampling.py
asserts membership.

Membership alone has two blind spots, both covered:
- a *vacuous* check — with top_k=0/top_p=1.0 the admissible set is the whole
  vocabulary and can never fail — so the mean admissible set size is reported
  and bounded (measured 4.3 of 151936, i.e. genuinely selective);
- a sampler silently degenerated to argmax, which is always inside the
  support — so divergences from greedy are counted against how many the
  filtered distribution predicts (measured 15 observed vs 16.58 expected).

Also audited: token ids in range, seeded replay on identical logits, and the
same membership check with per-request *tensor* top_k/top_p, which takes a
different kernel path (a scalar top_k on a large vocab only ever exercises the
top_k_first fast path).

smoke_test.py gains a sampling run pair alongside the greedy pair, asserting
all of the above plus seeded cross-process determinism and sampling/topk JIT
cache reuse. Greedy remains the default everywhere and its existing
assertions are untouched. `--skip-sampling` restores the old behavior.

Validated on B200 against flashinfer 0.6.16rc3 (which carries flashinfer-ai#4189's
`__launch_bounds__` gate): all bars green, reference_check unregressed.

AI-assisted (Claude Code).
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1e6c06c8-bf08-4c63-ab2a-776ea9bd6313

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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