perf(cuda): port sample_logits chain to a fused GPU sampler kernel#478
Merged
davide221 merged 17 commits intoJul 6, 2026
Merged
Conversation
The draft top-K + logsumexp kernel launched only n_positions (~15) blocks,
leaving most of the GPU's SMs idle, and kept its per-thread top-K in a
data-dependent insertion index that the compiler spilled to local memory and
re-read on every vocab element. Both made the ~9 MB vocab scan run at a small
fraction of peak DRAM bandwidth.
Rework into a split-K two-pass design:
- pass 1 (draft_topk_partial) splits each position's vocab scan across many
blocks (2D grid n_positions x split) so all SMs stay busy;
- pass 2 (draft_topk_combine) merges the per-split partials per position.
Template both kernels on K (compile-time) so the top-K stays register-resident
via a branchless unrolled bubble instead of spilling, and read logits as float4
(one coalesced 16-byte transaction per 4 logits) with a scalar fallback when a
row base is not 16-byte aligned (vocab % 4 != 0), preserving any-vocab
correctness. split is auto-tuned (env override DFLASH_TOPK_SPLIT).
Measured on an RTX 3090 (n=15, vocab=151936, K=8):
- GPU kernel time: 392 us -> 36.3 us (30.6 partial + 5.75 combine), 10.8x
- full call (kernel+sync+D2H): 0.407 ms -> 0.053 ms, 7.7x
Full-call speedup is 5.9-8.4x across n in {7,15,31,63}. Output is bit-for-bit
equivalent to the CPU reference (id_mismatches=0) across K in {1,2,4,8} and
both aligned and odd vocab; compute-sanitizer memcheck clean on both paths.
Adds bench_topk.cu, a standalone microbenchmark + CPU-reference correctness
harness (not wired into the build) used to profile and A/B this change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T55hNb5cgyCwNYnNAE1hun
Contributor
|
@pramodith @DeanoC it seems that the PR has some conflict, would be useful for us to be able to visualize them |
Contributor
|
@davide221 sorry the merge conflict was because a parallel worktree messed up the |
Contributor
|
New results after latest commit for coalescing the cuda reads and writes. End-to-End Baseline CPU only path: GPU Path: |
Contributor
|
LGTM. Potential non blocking bug: the feature has a build-time opt-out via DFLASH_GPU_SAMPLER, but test_gpu_sampler_cuda is registered for CUDA test builds without checking that option. If generated code adds feature-gated sources, it should gate the matching tests on the same option too, otherwise DFLASH_GPU_SAMPLER=OFF with tests enabled may fail to link. |
DeanoC
added a commit
to GeometricAGI/lucebox-hub
that referenced
this pull request
Jul 7, 2026
The fused GPU sampler (sample_logits chain: rep/freq/pres penalties -> softmax(temp) -> greedy/multinomial draw) added in Luce-Org#478 — its kernel (geometric_sampler_cuda.cu), header, the sampler.cpp / qwen35_backend.cpp dispatch, and test_gpu_sampler_cuda — was wired for the CUDA backend only. This enables it on HIP so AMD (gfx1100/gfx1151/gfx1201) samples on-device instead of falling back to the CPU chain. No kernel or dispatch changes; the whole diff is the HIP build wiring + shim: * CMakeLists: compile geometric_sampler_cuda.cu as LANGUAGE HIP under the hip backend (option DFLASH_GPU_SAMPLER, ON) and define DFLASH27B_HAVE_GPU_SAMPLER so the backends take the GPU dispatch branch; build test_gpu_sampler_cuda on hip too (the CUDA-spelled test compiles as HIP via hip_compat/, like test_flashprefill_kernels). * hip_compat/cuda_runtime.h: add cudaPointerAttributes / cudaPointerGetAttributes / cudaMemoryTypeDevice, and map CUDA's 32-bit-mask __shfl_xor_sync onto HIP's mask-less __shfl_xor (HIP's _sync form static_asserts against the implicit 32->64-bit mask promotion; the all-lanes-active wave32 reductions here don't need the mask, matching rms_norm_hip.cu). The kernel's 32-lane warp assumption holds on RDNA wave32. Validated on the AMD Radeon AI PRO R9700 (gfx1201, RDNA4, ROCm 7.1.1): * test_gpu_sampler_cuda: 22/22 pass (greedy/penalties match CPU argmax bit-for-bit; temp + top_p-assist draws match analytic softmax/nucleus). * per-call microbench (vocab=151936, 1000 iters), GPU sampling from the device logits tensor (the production decode path, logits_on_device=true): greedy (temp=0) 54.1 us vs CPU 183.4 us = 3.39x temp=0.8 (full vocab) 92.1 us vs CPU 218.8 us = 2.38x temp=0.8 rep_pen=1.2 181.4 us vs CPU 303.7 us = 1.67x (GPU+H2D is ~1.0x — copying 152k logits H2D per token eats the win, which is why the decode loop samples in place.) * dflash_server e2e (Qwen3.6-27B Q4_K_M + draft, temp=0.8): DFLASH_GPU_SAMPLE=1 vs =0 both coherent, no regression (21.9 vs 21.7 tok/s), no HIP errors.
This was referenced Jul 7, 2026
DeanoC
added a commit
to GeometricAGI/lucebox-hub
that referenced
this pull request
Jul 7, 2026
The split-K draft top-K + logsumexp kernel (geometric_draft_topk_cuda.cu, the spec-decode draft path) added alongside the sampler in Luce-Org#478 was wired for the CUDA backend only, so on AMD the draft top-K fell back to the CPU reference (extract_draft_topk). This enables the GPU kernel on HIP so AMD (gfx1100/gfx1151/gfx1201) uses it too. * CMakeLists: compile geometric_draft_topk_cuda.cu as LANGUAGE HIP under the hip backend and define DFLASH27B_HAVE_DRAFT_TOPK_CUDA so callers take the GPU path; build test_draft_topk_cuda on hip too (CUDA-spelled source compiles as HIP via hip_compat/). Stacked on the sampler HIP PR, which adds the shared hip_compat pointer-attribute shims this kernel also uses. * test_draft_topk_cuda: add a DFLASH_TOPK_BENCH-gated CPU-vs-GPU microbench (mirrors test_gpu_sampler_cuda's DFLASH_SAMPLER_BENCH) so the win is measurable on any backend. The kernel needs no source or shim changes: it has NO warp-level intrinsics (a plain kBlock-wide __syncthreads reduction, so no wavefront-size assumption), and its only runtime calls (cudaMalloc / cudaMemcpy / cudaPointer*) resolve through the existing shim. Validated on the AMD Radeon AI PRO R9700 (gfx1201, RDNA4, ROCm 7.1.1): * test_draft_topk_cuda: 10/10 pass — top-K ids bit-for-bit vs the CPU reference (id_mismatch=0) across Qwen vocab (151936) and n/K/temp shapes, plus the K>kMaxK CPU-fallback contract. max log-prob err <= 3.2e-4. * CPU-vs-GPU microbench (DFLASH_TOPK_BENCH=1, K=8, 500 iters, logits resident on device = the decode steady state; GPU time = launch+sync+D2H of the n*K result): n=15 vocab=151936 CPU 695.2 us -> GPU 164.9 us = 4.22x n=1 vocab=151936 CPU 594.4 us -> GPU 153.6 us = 3.87x
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.
Summary
Ports the CPU
sample_logitschain (repetition/frequency/presence penalty, softmax(temp), top_p nucleus, multinomial draw) to CUDA so the Qwen3.5 AR decode loop can sample straight off the device logits tensorinstead of paying a full vocab-wide (151,936-float) D2H copy every token.
Penalty application, the softmax reductions, and the draw are now one fused kernel (mode-selected: greedy / sample / emit-probs) using warp-shuffle block reductions, replacing what used to be a separate penalty kernel plus a shared-memory tree reduction.
top_ktruncation stays on the CPU but we usepartial_sortinstead of a full sort, and puretop_pisGPU-assisted: the GPU computes penalties+softmax and hands back the probability vector for the CPU to truncate.
CPU-side
top_palso changed independently of the GPU work: nucleus truncation no longer does a fullO(vocab log vocab)sort. It now usesnucleus_cutoff, anstd::nth_element-based recursive bisection thatfinds the cutoff index in
O(vocab)total work regardless of where the nucleus lands.The GPU path is on by default for CUDA builds; opt out with
DFLASH_GPU_SAMPLE=0.Impact
Kernel Level
DFLASH_SAMPLER_BENCH=1 ./test_gpu_sampler_cudaEnd-to-End
Baseline
Compute on CPU
Run via:
DFLASH_SAMP=0.8,1.0,0,1.1,42 DFLASH_GPU_SAMPLE=0 python -m server.scripts.bench_llm --bench HumanEvalNote: In the above command temp=0.8, top-p=1.0, top-k=0, rep_pen=1.1,r andom_seed=42
New Kernel
Run via:
DFLASH_SAMP=0.8,1.0,0,1.1,42 DFLASH_GPU_SAMPLE=1 python -m server.scripts.bench_llm --bench HumanEvalResults: An approx ~32% increase in tok/s with similar Acceptance Lengths.
Implementation
server/src/common/sampler.cpp— addednucleus_cutoff(nth_element-based O(vocab) bisection, replaces a full sort for top_p) anddraw_from_weights(deduplicates the final weighted CDF draw); wired the two GPU dispatch points (full-GPU for greedy/temp, GPU-assisted for pure top_p) intosample_logits.server/src/common/geometric_sampler_cuda.cu/h— new/rewritten: single fusedgeometric_sample_kernel(mode-selected greedy / sample / emit-probs) doing penalty application, softmax reductions, and the multinomial draw in one launch, with warp-shuffle block reductions and a per-devicepick_block_size.server/src/qwen35/qwen35_backend.cpp— AR decode now callsgeometric_sample_logits_cudadirectly on the device logits tensor when the sampler config is GPU-supported, skipping the vocab-wide D2H copy the CPU chain otherwise needs.server/CMakeLists.txt— added theDFLASH_GPU_SAMPLERbuild option (defaultON) that compilesgeometric_sampler_cuda.cuintodflash_common, and registered thetest_gpu_sampler_cudactest target.server/test/test_gpu_sampler_cuda.cpp— new correctness test: GPU vs CPU agreement for greedy, greedy+penalties, temperature-sample distribution, and top_k/top_p CPU-fallback signaling.server/test/test_dflash.cpp— added--samp=temp,top_p,top_k,rep_pen,seed[,freq,pres]to the positional (non-daemon) harness so benchmarks can exercise the sampler chain.server/scripts/bench_llm.py— addedDFLASH_SAMP(forwards the sampler tail totest_dflash --samp=) andDFLASH_N_SAMPLE(overrides prompts-per-dataset) env vars.README.md— documented GPU sampler coverage, runtime/build flags, and the benchmark table below.Runtime Flags / Configuration
Default-on paths:
DFLASH_GPU_SAMPLE— on by default on CUDA builds; handles greedy and plain temperature/penalty sampling entirely on GPU, and assists puretop_p.Disable path:
DFLASH_GPU_SAMPLE=0— opt out at runtime; every call falls back to the CPU chain.-DDFLASH_GPU_SAMPLER=OFF(CMake option, defaultON) — dropgeometric_sampler_cuda.cufrom the build entirely.Debug/profiling-only flags:
--samp=temp,top_p,top_k,rep_pen,seed[,freq,pres](test_dflashpositional harness) — exercise the sampler chain instead of greedy decode.DFLASH_SAMP=temp,top_p,top_k,rep_pen,seed[,freq,pres]/DFLASH_N_SAMPLE=N(bench_llm.py) — forward the same sampler tail to every DFlash bench call, and override the per-dataset prompt count.top_k(with or withouttop_p) is intentionally never routed to the GPU— its CPU
partial_sortcost scales withk, not vocab, and a GPU roundtrip (kernel launch + D2H copy) measured as a net regression, not just a
non-win. This is a deliberate, measurement-driven exclusion, not a gap.
Notes
top_psupport directly on the GPU kernel (rather than GPU-assisted) isdeliberately out of scope for this PR;