Skill: Write a Triton Sampling Kernel
SkillAI & modelsGuide the agent through implementing a Triton kernel for LLM decode-time token sampling: take a [batch, vocab] logits tensor, apply per-request temperature, top-k, and top-p (nucleus) filtering, renormalize, and draw one token per request.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Skill: Write a Triton Sampling Kernel skill
About this capability
Open source skill library for AI coding agents to write, optimize, and debug high performance compute kernels across CUDA, Triton, and quantized workloads.
What this skill tells your AI
The instructions your AI receives, as published by tensormux/kernel-skills in skills/inference/write-triton-sampling-kernel/SKILL.md and read by ahel’s review.
Purpose
Guide the agent through implementing a Triton kernel for LLM decode-time token sampling: take a [batch, vocab] logits tensor, apply per-request temperature, top-k, and top-p (nucleus) filtering, renormalize, and draw one token per request. This is the last hot kernel on every decode step — it runs once per generated token, so latency directly translates into tokens/second.
Use this when
- You need a sampling strategy that vLLM, SGLang, TGI, or FlashInfer do not expose (typical-p, mirostat, classifier-free guidance, fused repetition penalty, structured-generation logit bias, contrastive decoding).
- You need heterogeneous per-request sampling — each request has its own
T,k,p, seed, and possibly its own logit-bias mask — and you want one fused kernel rather than N samplers. - You are willing to special-case the greedy path (T == 0 or top-k == 1) to skip softmax and sort.
- Decode batch size is large enough (B >= 8) that one-program-per-request is worthwhile. For B == 1, a CPU-side argmax/multinomial is usually fine.
Do not use this when
- A vendor sampler covers your case. vLLM's
Samplerand FlashInfer'stop_k_top_p_sampling_from_probsare heavily tuned and handle edge cases (extremely peaked distributions, ties, deterministic argmax fallback). Re-implementing without a concrete reason is a likely source of subtle bias. - You need only argmax.
logits.argmax(-1)from PyTorch is competitive and avoids every numerical pitfall in this skill. - The strategy requires global communication across the batch (beam search, speculative decoding verification). Those are not multinomial-per-request.
- You need provably uniform reproducibility across hardware. RNG semantics, cumsum reduction order, and sort tie-breaking are all platform-dependent.
Inputs the agent should gather first
Before writing any code, confirm:
- Vocab size V. Typical: 32k (Llama-2), 128k (Llama-3), 256k (Gemma). Determines whether the row fits in one BLOCK or needs multi-block streaming.
- Batch size B. Number of concurrent requests in the decode step. Each request maps to one program.
- Per-request sampling params. Are
T,k,pscalars (uniform) or tensors of shape[B](heterogeneous)? Heterogeneous is the realistic case in continuous-batching servers. - Logits dtype. Almost always fp16 or bf16 from the LM head. Sampling internally promotes to fp32.
- RNG source. Stateful Philox seed/offset (advanced once per decode step) or a precomputed
[B]tensor of uniforms. Stateful is more flexible; precomputed is simpler and easier to test. - Greedy fallback policy. Is
T == 0legal? Istop_k == 1legal? Both must short-circuit to argmax. - Logit bias / mask. Per-request additive bias (e.g., grammar-constrained decoding) is added to logits before temperature scaling.
- Maximum top_k. A hard upper bound (e.g.,
K_MAX = 1024) lets you pick a sort strategy at compile time. Without a bound, you cannot size a fixed on-chip sort buffer.
Required reasoning process
-
Parallelism axis. Sampling is embarrassingly parallel across the batch. One Triton program per request:
req_idx = tl.program_id(0), grid(B,). All work for one request — temperature, softmax, top-k, top-p, multinomial draw — happens inside that program. No inter-program communication. -
Vocab tiling. Choose
BLOCK_Vas a power of 2 (4096 or 8192 typical). For V <= BLOCK_V, the row fits in one block. For V > BLOCK_V (common at V >= 32k), the program loops overceil(V / BLOCK_V)chunks. Running state (max, sum, top-k buffer) lives in registers across iterations. -
Short-circuit greedy. Read
T = T_ptr[req_idx]andk = k_ptr[req_idx]. IfT == 0.0ork == 1, run an argmax-only pass: streaming reduction over V tracking(max_logit, max_idx), then storemax_idx. Skip softmax, sort, and RNG. Tie-break deterministically by smaller index. -
Temperature and logit bias before softmax. Inside each chunk:
chunk = (chunk + bias_chunk) / T. Promote to fp32 first — when T is small (e.g., 0.1 multiplies logits by 10), fp16 dynamic range is insufficient. -
Numerically stable softmax over V. Same problem as
write-triton-softmax-kernel. For V <= BLOCK_V, single-pass online softmax. For V > BLOCK_V, online algorithm: trackrunning_maxandrunning_sum, rescalerunning_sumbyexp(old_max - new_max)whenever the max updates. Accumulate in fp32. -
Top-k before top-p. Top-p alone requires sorting all V probabilities — too expensive at V >= 32k. Apply top-k first with a moderate K (50–1024), then top-p on the K survivors. If the user's k is unset, treat it as
K_MAX— caps sort cost without changing semantics for typical p. -
Top-k as streaming partial sort. Maintain a fixed-size sorted buffer of size K in registers. For each streamed value, if it exceeds the current min of the buffer, replace the min and re-sort. For
K <= 256, an in-register sorted array with binary insertion is competitive on Triton. For larger K, prefer radix-select or fall back to FlashInfer — pure Tritontl.sortover the full vocab is too slow at V = 128k. Output:top_probs[K]andtop_indices[K], sorted descending. -
Top-p on the K survivors. Read
p = p_ptr[req_idx]. Cumsum overtop_probs(already sorted descending). Find the smallest prefix withcumsum >= p; keep that prefix, zero the rest. Always keep at least one token — iftop_probs[0] > p, the top-1 alone forms the nucleus. Missing this guard produces an all-zero distribution. -
Renormalize. After top-k and top-p masking, surviving probs no longer sum to 1. Divide by
sum(top_probs_after_masking). Skipping this is the single most common sampling-bias bug. -
Multinomial draw. Generate
u ~ U(0, 1). Walk the sorted, masked, renormalizedtop_probsaccumulating cumsum; pick the first indexjwithcumsum >= u. Chosen token istop_indices[j]. With Philox:tl.rand(seed, offset + req_idx)or precomputedu_ptr[req_idx]. The offset must advance every decode step — reusing the same offset gives identical samples each step. -
Store.
tl.store(out_ptr + req_idx, token_id).
Kernel design rules
- One program per request. No cross-request communication. Grid is
(B,). - All reductions, softmax, cumsum, and renormalization run in fp32. Logits arrive as fp16/bf16; cast on load. Keep
probsin registers — do not write back to global memory. BLOCK_VandK_MAXaretl.constexpr.K_MAXis the compile-time upper bound on top-k; the runtimekis masked against it. Without a compile-time bound, the sort buffer cannot be sized.- The greedy path (
T == 0ork == 1) is a separateifbranch. Do not share code with the sampling path — determinism, dtype handling, and exit conditions differ. - Out-of-bounds vocab loads must use
other=-float('inf')so they collapse to zero probability. - Logit bias is added in fp32 before division by T. Adding after softmax is incorrect — it biases probabilities directly, not the energy.
- The cumsum used for the multinomial draw must be over the renormalized array. A stale cumsum (computed before renormalization) is one of the easiest bugs to ship.
- RNG: Philox via
tl.rand(seed, offset). The(seed, offset)pair must be unique per(request, decode_step). Common pattern:seed = global_seed,offset = step_idx * B + req_idx. - For heterogeneous batches, load every per-request param (
T,k,p,seed) from a[B]tensor inside the program. Do not pass them as kernel scalars unless the batch is genuinely homogeneous.
Correctness requirements
- Renormalization after filtering is mandatory. After top-k and top-p, surviving probs do not sum to 1. A cumsum-vs-uniform draw against an unnormalized vector is silently biased. Always divide by the post-filter sum.
- Top-p must keep at least one token. If
top_probs[0] >= p, the nucleus is the top-1 alone. Use a guard so the top-1 is never masked out. - Greedy path must be exact. When
T == 0.0, no softmax, no RNG, no sort — pure argmax with smallest-index tie-breaking. Softmax with T = 0 is undefined; short-circuit before division. - Max subtraction before exp. Same as softmax: subtract running max before
expto prevent fp32 overflow above ~88. After temperature scaling with small T, raw logits often exceed this. - Top-k masking is consistent. Pick one representation: temperature → softmax → top-k on probs → top-p on probs → renormalize → sample. Applying top-k as a logit mask before softmax is also valid but requires
-infmasking, with max-subtraction running after. - RNG offset advances every decode step. A common bug: same offset every step, same
uevery step. The driver must increment between calls. - Cumsum precision. Cumsum over K = 1024 entries in fp16 drifts: each value ~1/K and
K * eps_fp16is order 0.5. The final entry can be 0.5 instead of 1.0. Cumsum must run in fp32. - Tie-breaking in argmax. Equal logits are common with bias masks that set allowed tokens to a fixed value. Define and implement smallest-index-wins;
tl.max's tie order is not part of its contract.
Performance requirements
The agent must reason through:
- Memory bandwidth. Sampling reads each logit at most twice (max, then masked exp/sort) and writes one int per request. At B = 64, V = 128k, fp16 logits this is ~16 MB per pass — small enough that per-request overhead, not HBM bandwidth, dominates at high B.
- Sort cost dominates. Streaming partial top-k over V is O(V log K). At V = 128k, K = 1024 that is ~2M comparisons per request. Tightening K via a hard upper bound is the highest-leverage optimization.
- Per-request divergence. Heterogeneous params mean programs do different work — one request greedy, another full top-k+top-p. SM-level load balancing suffers. If the batch is bimodal (half greedy, half sampling), dispatch them as two kernels.
- fp32 cost. All reductions and the cumsum are fp32. Non-negotiable for correctness; do not "optimize" by dropping to fp16.
- Compare against vendor. Benchmark against vLLM's
Sampleror FlashInfer'stop_k_top_p_sampling_from_probs. Matching their throughput at V = 128k is a real result; significantly slower usually means top-k is being implemented as a fulltl.sort. - Latency budget. End-to-end decode for a 7B model on H100 is ~10 ms/token. The sampler should be < 5% of that — sub-0.5 ms per decode step for the whole batch.
Output format
The agent should produce:
- The Triton kernel with
@triton.jit, taking:logits_ptr,out_token_ids_ptr, per-request param pointers (T_ptr,k_ptr,p_ptr, optionallyseed_ptr,offset), optionallogit_bias_ptr,B,V,logits_row_stride, andBLOCK_V: tl.constexpr,K_MAX: tl.constexpr. - The greedy short-circuit path as an explicit branch on
T == 0.0ork == 1. - The sampling path: temperature scale → online softmax → streaming partial top-k → top-p prefix-cumsum mask → renormalize → multinomial draw via cumsum-vs-uniform.
- The Python launcher that builds the per-request param tensors, picks
BLOCK_VandK_MAXbased on V and the user-specified max k, and computes the grid as(B,). Driver code increments the RNG offset between calls. - A correctness test comparing against a reference PyTorch implementation: for fixed seed, the chosen token must match the reference for several
(T, k, p)settings. Also test the limit cases:T = 0matches argmax;k = 1matches argmax;k = Vandp = 1.0matches plain multinomial sampling from softmax. - A statistical test for the sampling path: with a known logit distribution, draw N >> 1 samples and verify the empirical histogram matches the post-filter probabilities within chi-squared tolerance.
- Documented assumptions: max supported K, behavior on
T < 0orp > 1(reject or clamp), tie-breaking policy.
Common failure modes
- Skipped renormalization. Top-k or top-p applied, surviving probs sent directly to the cumsum-vs-uniform draw without dividing by the post-filter sum. Outputs drift from the intended distribution; invisible without statistical tests.
- Stale RNG offset. Same
(seed, offset)every decode step. Sameu, same sample relative to the same probabilities. Manifests as repetition or as outputs that look almost-greedy without being greedy. - Greedy path bypassed at T = 0.
logits / Tproduces inf/NaN, softmax produces NaN, multinomial picks whatever lane resolves first. Always testT == 0.0and route to argmax. - Top-k mask before max-subtraction with wrong fill. Masked-out entries left at original logits or set to 0 corrupt the max reduction. Use
-inf(or a large negative) so masked entries collapse to zero probability and do not skew the max. - fp16 cumsum. See Correctness — drifts to ~0.5 over K = 1024. Always fp32.
- Top-p with no min-keep guard. Peaked distribution + tight
p(e.g.,top_probs[0] = 0.95,p = 0.9) with naivecumsum > pexcludes the top-1 and produces an all-zero nucleus. Always keep at least one token. - Full
tl.sortover the vocab. Compile time and runtime blow up at V = 128k. Use a streaming partial sort with a fixedK_MAXbuffer. - Heterogeneous batching ignored. Kernel takes a scalar
Tand applies it to every request. The moment requests have different temperatures (normal case), all but one request samples wrong. Thread per-request params through[B]tensors. - Logit bias added after softmax. Adding to probabilities does not produce the bias-conditioned distribution. Symptom: structured-generation grammars allow tokens that should be masked.
- Non-deterministic argmax tie-breaking. Equal logits (common with bias masks). The kernel returns whichever lane resolved first, which can vary across runs. Implement smallest-index-wins explicitly.
Review checklist
- Greedy path (
T == 0.0ork == 1) short-circuits to argmax with deterministic tie-breaking; no softmax, no RNG, no sort on this path. - Logits are cast to fp32 before temperature scaling, max-subtraction, exp, sum, cumsum, and renormalization.
- Out-of-bounds vocab loads use
other=-float('inf'). - Top-k is implemented as a fixed-size streaming partial sort, with K bounded by a compile-time
K_MAX. - Top-p is applied after top-k, on the sorted survivors, and always keeps at least the top-1 token.
- Probabilities are renormalized after top-k and top-p masking, before the multinomial draw.
- The cumsum used for the draw is over the renormalized, masked probs — not a stale pre-filter cumsum.
- RNG
(seed, offset)is unique per(request, decode step); the driver advances the offset between calls. - Logit bias, if any, is added on logits before division by T, never on probabilities.
- Per-request
T,k,p, and seed are loaded from[B]tensors inside the kernel; no scalar params for heterogeneous batches. - Correctness test covers:
T = 0matches argmax,k = 1matches argmax,k = V, p = 1.0matches reference multinomial-from-softmax, and a chi-squared empirical histogram check. - Behavior on illegal inputs (
T < 0,p > 1,k = 0) is documented and either rejected at the launcher or clamped consistently. - Performance is compared against vLLM
Sampleror FlashInfer top-k/top-p sampling, and the result is reported as a measurement, not a claim.
Signals
- GitHub stars
- 75
- Forks
- 8
- Last commit
- Jun 2026
Advanced
- Catalog kind
- skill
- Gateway key
write-triton-sampling-kernel- Source
- github.com/tensormux/kernel-skills