Repository navigation
Conversation
…ampling TorchSampling draws argmax(p / E) with E = -log(U) and float32 U. A float32 uniform has 24 bits near 1 (and Triton's request-seeded path 31 bits near 0), so E only spans about 20 nats: a token more than ~20 nats below the row's top token can never be drawn, and tokens 16-20 nats below it are drawn at the wrong rate. On rows [0, -D, ..., -D] (V = 131072), 2e5-1e6 draws per point: r = e^-D expected unseeded seeded (FlashInfer logits kernel) 1e-7 2597 2937 1527 1322 1e-8 1313 37 7 1 1e-9 132 0 0 0 Add InferenceConfig.fp64_sampling_noise (CLI --inference-dynamic-batching-fp64-sampling-noise, default off). When set, the torch backend runs the race in float64: unseeded rows use the shared generator's float64 exponential (53-bit uniforms) and request-seeded rows use two Philox words for a 52-bit midpoint uniform, so the noise spans ~40 nats. The drawn law is still the float32 softmax. Seeded draws stay keyed by (seed, absolute position, vocabulary index) and batch-position invariant. The FlashInfer backend draws float32 uniforms inside its kernels, so the option requires sampling_backend='torch'. Signed-off-by: wdykas <wdykas@nvidia.com>
Contributor
Author
|
/ok to test 2b66a7f |
This was referenced Oct 29, 2026
This branch was previously deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Adds an opt-in
fp64_sampling_noisefor the torch sampling backend. When it is on, low-probability tokens are drawn at their softmax rate: float64 race noise moves the representable tail from about 20 nats below a row's top token to about 40 nats. Default behavior and speed are unchanged.Problem
TorchSampling.sample_from_logitsdrawsargmax(p / E)withE = -log(U)(megatron/core/inference/sampling/torch_sampling.py:136-159at 13a9d46).Uis a float32 uniform, fromq.exponential_()for unseeded rows or fromtl.uint_to_uniform_floatinrequest_seed_noise.py:35-39for request-seeded rows. A float32 uniform has only 24 bits near 1, so the smallest nonzeroEis about 6e-8, and the largest is about 22 (about 87 for the seeded path's clamped zero). A tokenDnats below the top token wins only whenE_top / E_token > e^D. That ratio cannot exceed about e^20, so:In wide-vocabulary rows those tail tokens add up. The sampled law is then not the softmax that
log_probs_kernelreports, and no log-prob check can detect it. The code comment says the race keeps the low-probability tail; the noise precision is what loses it.Reproduced on
main(13a9d46), GB200. Rows are[0, -D, ..., -D]in float32 with V = 131072. Under the exact law, a draw misses token 0 with probability(V-1) r / (1 + (V-1) r),r = e^-D. Each cell gives the number of draws that missed token 0; p is the two-sided Poisson p-value.sampling_from_logitsThe change
InferenceConfig.fp64_sampling_noise(defaultFalse); CLI--inference-dynamic-batching-fp64-sampling-noise. The controller passes it toTorchSampling(..., fp64_noise=...).TorchSampling.sample_from_logits(..., fp64_noise=True)casts the float32 softmax to float64 and runs the same race in float64:exponential_(53-bit uniforms);fill_request_seed_noiseon a float64 buffer. The Triton kernel takes two Philox words (it used one before and dropped the other three) and forms a 52-bit midpoint uniform(k + 1/2) / 2^52, exact in float64. The noise then lies in [1.1e-16, 36.7] and needs no endpoint clamp. The variate is still keyed by (request seed, absolute position, vocabulary index), so seeded draws stay batch-position invariant.fp64_sampling_noise=Truewithsampling_backend='flashinfer'raises inInferenceConfig.__post_init__. A FlashInfer-side fix would belong in FlashInfer.Tests
tests/unit_tests/inference/sampling/test_fp64_sampling_noise.py(GPU):test_fp64_noise_matches_tail_rate: unseeded and request-seeded, at r = 1e-7, 1e-8 and 1e-9, 262k draws of V = 131072 each. The miss count must lie inside the two-sided 1e-6 Poisson interval of the exact rate.test_fp32_noise_misses_tail(non-vacuity): the default float32 noise falls below that interval at r = 1e-9 (expected 34, observed 0).test_fp64_noise_chi_square: 1M draws over a 16-token row; chi-square below the 1e-6 quantile (56.5, 15 dof). Unseeded and seeded.test_fp64_seeded_draws_are_batch_position_invariant: for no filter, top-p 0.9, and top-k 50 at T 0.7, seeded draws are bitwise equal across repeated calls, single-row calls, and a permuted batch with unseeded batch mates.test_sample_kernel_uses_configured_noise:TorchSampling(..., fp64_noise=True).sample_kernelequals the float64sample_from_logits.test_fp64_noise_requires_torch_backend.tests/unit_tests/determinism/kernels/test_request_seed_sampling.py::test_fp64_request_noise_replays_and_preserves_logical_coordinates: the float64 Philox noise replays bit-exactly under side-stream contention, and single-row launches equal batched ones. It also checks the noise range and that the float32 stream is unchanged. The manifest entryinference_request_seed_samplingnotes both widths.Run on GB200, one GPU: the new file plus
test_request_seed_sampling.py(both),test_torch_sampling.py, andtest_manifest.py: 323 passed.test_inference_config.py,training/configandtest_inference_initialization.py: 388 passed. 3 failures intraining/config/test_instantiate_utils.py/test_target_allowlist.pyalso fail on unmodifiedmainin this environment. Black, isort, ruff and pylint (10.00/10) pass on the changed files.CUDA graphs: the torch sampler is not captured in a graph on
main(TorchSampling.sample_kernelignoreseager/cache_key), so the option adds no graph path.Cost
Sampler time per call,
sample_from_logitson bf16 logits, no filter, median of 30, GB200:Below about 64 rows the cost is within launch overhead. At larger batches it is about +1.1 µs per row at V = 131072, because the float64 noise and ratio are materialized: twice the bytes of the float32 path. A follow-up could avoid that with a fused kernel that generates the noise in registers and reduces each row in place. It would need several blocks per row, since one program per row is latency-bound at small batch.
Issue tracking
Standalone fix; no linked issue. Related: #7182 (float32 exponential race), #7828 (request-local seeds), #8290 (request-local FlashInfer seeds).
Contribution process
Pre-checks
jit_fuser/torch.compile, CUDA extension, TE or external-library dispatch, or a scatter/index accumulation), I have added or updated its bit-exact determinism test and registered it intests/unit_tests/determinism/kernels/manifest.py(guide)