CharlesCNorton commited on
Commit ·
9aa33e7
1
Parent(s): 726f31a
Add logits-processor source, tests, and model card
Browse files- CARD.md +70 -0
- README.md +70 -0
- build.toml +23 -0
- flake.nix +17 -0
- logits_processor_cuda/logits_processor.cu +204 -0
- tests/test_logits_processor.py +71 -0
- torch-ext/logits_processor/__init__.py +63 -0
- torch-ext/torch_binding.cpp +17 -0
- torch-ext/torch_binding.h +17 -0
CARD.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
library_name: kernels
|
| 3 |
+
license: apache-2.0
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
# logits-processor
|
| 7 |
+
|
| 8 |
+
The generation last-mile as loadable [`kernels`](https://github.com/huggingface/kernels):
|
| 9 |
+
guided-decoding masking and fused sampling, the two hot-path ops between a
|
| 10 |
+
model's logits and the next token that the Kernel Hub did not carry.
|
| 11 |
+
|
| 12 |
+
```python
|
| 13 |
+
from kernels import get_kernel
|
| 14 |
+
lp = get_kernel("phanerozoic/logits-processor")
|
| 15 |
+
|
| 16 |
+
# 1) structured / guided decoding: -inf the tokens a grammar disallows
|
| 17 |
+
lp.apply_token_bitmask(logits, bitmask) # logits [B, V], bitmask [B, ceil(V/32)] int32
|
| 18 |
+
|
| 19 |
+
# 2) fused temperature + top-k ∩ top-p ∩ min-p + multinomial draw
|
| 20 |
+
tokens = lp.sample(logits, temperature=0.7, top_k=50, top_p=0.9, min_p=0.0, seed=step)
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
## Ops
|
| 24 |
+
|
| 25 |
+
### `apply_token_bitmask(logits, bitmask) -> logits`
|
| 26 |
+
Sets the logits of disallowed tokens to `-inf`, in place, from a packed allow-mask.
|
| 27 |
+
Bit `1` means the token is **allowed** (XGrammar convention). `logits` is `[B, V]`
|
| 28 |
+
in float/half/bf16; `bitmask` is `[B, ceil(V/32)]` int32. This is the op behind
|
| 29 |
+
constrained JSON and tool-calling output — apply it, then `sample`.
|
| 30 |
+
|
| 31 |
+
### `sample(logits, temperature=1.0, top_k=0, top_p=1.0, min_p=0.0, seed=0, offset=0) -> tokens`
|
| 32 |
+
One `logits -> token` op: per-row temperature scaling, then the **intersection**
|
| 33 |
+
of top-k, top-p (nucleus), and min-p filtering, then a multinomial draw.
|
| 34 |
+
|
| 35 |
+
- `top_k <= 0`, `top_p >= 1`, `min_p <= 0` each disable that filter.
|
| 36 |
+
- `temperature <= 0` is greedy (argmax).
|
| 37 |
+
- Every sampling parameter is per-row: pass a scalar to broadcast, or a length-`B`
|
| 38 |
+
tensor for a heterogeneous batch — continuous batching with a different grammar,
|
| 39 |
+
temperature, or nucleus per request.
|
| 40 |
+
- The draw is a counter-based RNG keyed by `(seed, row, offset)`: deterministic
|
| 41 |
+
and replayable. Advance `offset` (or `seed`) per decoding step.
|
| 42 |
+
- Returns int64 token ids `[B]`.
|
| 43 |
+
|
| 44 |
+
## Correctness
|
| 45 |
+
|
| 46 |
+
Verified through `get_kernel` on A100 (sm80), A10G (sm86), L4 (sm89), H200 (sm90),
|
| 47 |
+
and RTX PRO 6000 (sm120):
|
| 48 |
+
|
| 49 |
+
- the bitmask sets exactly the disallowed logits to `-inf` in float / half / bf16,
|
| 50 |
+
including a vocabulary size that is not a multiple of 32;
|
| 51 |
+
- greedy and `top_k=1` return the argmax;
|
| 52 |
+
- the sampled token always lies in the exact top-k / top-p / min-p keep-set and in
|
| 53 |
+
their intersection, checked against an independent reference over many draws;
|
| 54 |
+
- the empirical sampling distribution matches the filtered, renormalized softmax
|
| 55 |
+
reference to within 0.002 (0.0003 at a 32k vocabulary);
|
| 56 |
+
- draws are deterministic in `(seed, offset)` and change when `offset` advances;
|
| 57 |
+
- per-row temperature / top-k / top-p / min-p are honored independently across a
|
| 58 |
+
batch, and a bitmask followed by `sample` never draws a disallowed token.
|
| 59 |
+
|
| 60 |
+
## Status
|
| 61 |
+
|
| 62 |
+
v1 is correctness-first: sampling is a single descending sort of the softmax
|
| 63 |
+
probabilities followed by a per-row prefix cutoff and inverse-CDF draw. The sort
|
| 64 |
+
dominates; a radix-select over the top-p mass is the first planned optimization.
|
| 65 |
+
Built for sm80 / sm86 / sm89 / sm90 / sm100 / sm120; certified on all of these
|
| 66 |
+
except sm100, for which no B200 was available to run the check.
|
| 67 |
+
|
| 68 |
+
## License
|
| 69 |
+
|
| 70 |
+
Apache-2.0.
|
README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
library_name: kernels
|
| 3 |
+
license: apache-2.0
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
# logits-processor
|
| 7 |
+
|
| 8 |
+
The generation last-mile as loadable [`kernels`](https://github.com/huggingface/kernels):
|
| 9 |
+
guided-decoding masking and fused sampling, the two hot-path ops between a
|
| 10 |
+
model's logits and the next token that the Kernel Hub did not carry.
|
| 11 |
+
|
| 12 |
+
```python
|
| 13 |
+
from kernels import get_kernel
|
| 14 |
+
lp = get_kernel("phanerozoic/logits-processor")
|
| 15 |
+
|
| 16 |
+
# 1) structured / guided decoding: -inf the tokens a grammar disallows
|
| 17 |
+
lp.apply_token_bitmask(logits, bitmask) # logits [B, V], bitmask [B, ceil(V/32)] int32
|
| 18 |
+
|
| 19 |
+
# 2) fused temperature + top-k ∩ top-p ∩ min-p + multinomial draw
|
| 20 |
+
tokens = lp.sample(logits, temperature=0.7, top_k=50, top_p=0.9, min_p=0.0, seed=step)
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
## Ops
|
| 24 |
+
|
| 25 |
+
### `apply_token_bitmask(logits, bitmask) -> logits`
|
| 26 |
+
Sets the logits of disallowed tokens to `-inf`, in place, from a packed allow-mask.
|
| 27 |
+
Bit `1` means the token is **allowed** (XGrammar convention). `logits` is `[B, V]`
|
| 28 |
+
in float/half/bf16; `bitmask` is `[B, ceil(V/32)]` int32. This is the op behind
|
| 29 |
+
constrained JSON and tool-calling output — apply it, then `sample`.
|
| 30 |
+
|
| 31 |
+
### `sample(logits, temperature=1.0, top_k=0, top_p=1.0, min_p=0.0, seed=0, offset=0) -> tokens`
|
| 32 |
+
One `logits -> token` op: per-row temperature scaling, then the **intersection**
|
| 33 |
+
of top-k, top-p (nucleus), and min-p filtering, then a multinomial draw.
|
| 34 |
+
|
| 35 |
+
- `top_k <= 0`, `top_p >= 1`, `min_p <= 0` each disable that filter.
|
| 36 |
+
- `temperature <= 0` is greedy (argmax).
|
| 37 |
+
- Every sampling parameter is per-row: pass a scalar to broadcast, or a length-`B`
|
| 38 |
+
tensor for a heterogeneous batch — continuous batching with a different grammar,
|
| 39 |
+
temperature, or nucleus per request.
|
| 40 |
+
- The draw is a counter-based RNG keyed by `(seed, row, offset)`: deterministic
|
| 41 |
+
and replayable. Advance `offset` (or `seed`) per decoding step.
|
| 42 |
+
- Returns int64 token ids `[B]`.
|
| 43 |
+
|
| 44 |
+
## Correctness
|
| 45 |
+
|
| 46 |
+
Verified through `get_kernel` on A100 (sm80), A10G (sm86), L4 (sm89), H200 (sm90),
|
| 47 |
+
and RTX PRO 6000 (sm120):
|
| 48 |
+
|
| 49 |
+
- the bitmask sets exactly the disallowed logits to `-inf` in float / half / bf16,
|
| 50 |
+
including a vocabulary size that is not a multiple of 32;
|
| 51 |
+
- greedy and `top_k=1` return the argmax;
|
| 52 |
+
- the sampled token always lies in the exact top-k / top-p / min-p keep-set and in
|
| 53 |
+
their intersection, checked against an independent reference over many draws;
|
| 54 |
+
- the empirical sampling distribution matches the filtered, renormalized softmax
|
| 55 |
+
reference to within 0.002 (0.0003 at a 32k vocabulary);
|
| 56 |
+
- draws are deterministic in `(seed, offset)` and change when `offset` advances;
|
| 57 |
+
- per-row temperature / top-k / top-p / min-p are honored independently across a
|
| 58 |
+
batch, and a bitmask followed by `sample` never draws a disallowed token.
|
| 59 |
+
|
| 60 |
+
## Status
|
| 61 |
+
|
| 62 |
+
v1 is correctness-first: sampling is a single descending sort of the softmax
|
| 63 |
+
probabilities followed by a per-row prefix cutoff and inverse-CDF draw. The sort
|
| 64 |
+
dominates; a radix-select over the top-p mass is the first planned optimization.
|
| 65 |
+
Built for sm80 / sm86 / sm89 / sm90 / sm100 / sm120; certified on all of these
|
| 66 |
+
except sm100, for which no B200 was available to run the check.
|
| 67 |
+
|
| 68 |
+
## License
|
| 69 |
+
|
| 70 |
+
Apache-2.0.
|
build.toml
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[general]
|
| 2 |
+
name = "logits-processor"
|
| 3 |
+
version = 1
|
| 4 |
+
edition = 5
|
| 5 |
+
license = "Apache-2.0"
|
| 6 |
+
backends = [
|
| 7 |
+
"cuda",
|
| 8 |
+
]
|
| 9 |
+
|
| 10 |
+
[general.hub]
|
| 11 |
+
repo-id = "phanerozoic/logits-processor"
|
| 12 |
+
|
| 13 |
+
[torch]
|
| 14 |
+
src = [
|
| 15 |
+
"torch-ext/torch_binding.cpp",
|
| 16 |
+
"torch-ext/torch_binding.h",
|
| 17 |
+
]
|
| 18 |
+
|
| 19 |
+
[kernel.logits_processor]
|
| 20 |
+
backend = "cuda"
|
| 21 |
+
cuda-capabilities = ["8.0", "8.6", "8.9", "9.0", "10.0", "12.0"]
|
| 22 |
+
depends = ["torch"]
|
| 23 |
+
src = ["logits_processor_cuda/logits_processor.cu"]
|
flake.nix
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
description = "Flake for logits-processor kernel";
|
| 3 |
+
|
| 4 |
+
inputs = {
|
| 5 |
+
builder.url = "github:huggingface/kernels";
|
| 6 |
+
};
|
| 7 |
+
|
| 8 |
+
outputs =
|
| 9 |
+
{
|
| 10 |
+
self,
|
| 11 |
+
builder,
|
| 12 |
+
}:
|
| 13 |
+
builder.lib.genKernelFlakeOutputs {
|
| 14 |
+
inherit self;
|
| 15 |
+
path = ./.;
|
| 16 |
+
};
|
| 17 |
+
}
|
logits_processor_cuda/logits_processor.cu
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// logits-processor: the generation last-mile as loadable kernels.
|
| 2 |
+
//
|
| 3 |
+
// Two ops that every serving stack needs on the hot path but that the Kernel Hub
|
| 4 |
+
// did not carry:
|
| 5 |
+
//
|
| 6 |
+
// apply_token_bitmask_inplace - structured/guided decoding. Given a packed
|
| 7 |
+
// allow-mask per request (XGrammar convention: bit 1 = token allowed), set
|
| 8 |
+
// every disallowed logit to -inf in place, so the following softmax cannot
|
| 9 |
+
// select it. This is the kernel behind constrained JSON / tool-calling.
|
| 10 |
+
//
|
| 11 |
+
// sample - temperature, then the intersection of top-k / top-p (nucleus) /
|
| 12 |
+
// min-p, then a multinomial draw, fused into one logits -> token op with a
|
| 13 |
+
// counter-based RNG (deterministic in (seed, row, offset)). temperature <= 0
|
| 14 |
+
// is greedy. The filters are computed on a single descending sort of the
|
| 15 |
+
// row so their cutoffs are all prefix lengths, and the smallest wins.
|
| 16 |
+
//
|
| 17 |
+
// v1 favors being obviously correct over peak throughput: one segmented sort of
|
| 18 |
+
// the softmax probabilities, then a per-row prefix walk for the cutoff and the
|
| 19 |
+
// inverse-CDF draw. The sort dominates; replacing it with a radix-select over
|
| 20 |
+
// the top-p mass is the first optimization once this certifies on GPU.
|
| 21 |
+
#include <ATen/ATen.h>
|
| 22 |
+
#include <ATen/cuda/CUDAContext.h>
|
| 23 |
+
#include <c10/cuda/CUDAGuard.h>
|
| 24 |
+
#include <cuda_fp16.h>
|
| 25 |
+
#include <cuda_bf16.h>
|
| 26 |
+
#include <cub/cub.cuh>
|
| 27 |
+
#include <cstdint>
|
| 28 |
+
#include <math.h>
|
| 29 |
+
|
| 30 |
+
namespace {
|
| 31 |
+
constexpr int TPB = 256;
|
| 32 |
+
|
| 33 |
+
__device__ __forceinline__ float to_f(float x) { return x; }
|
| 34 |
+
__device__ __forceinline__ float to_f(__half x) { return __half2float(x); }
|
| 35 |
+
__device__ __forceinline__ float to_f(__nv_bfloat16 x) { return __bfloat162float(x); }
|
| 36 |
+
__device__ __forceinline__ void set_ninf(float& x) { x = -INFINITY; }
|
| 37 |
+
__device__ __forceinline__ void set_ninf(__half& x) { x = __float2half(-INFINITY); }
|
| 38 |
+
__device__ __forceinline__ void set_ninf(__nv_bfloat16& x) { x = __float2bfloat16(-INFINITY); }
|
| 39 |
+
|
| 40 |
+
__device__ __forceinline__ unsigned triple32(unsigned x) {
|
| 41 |
+
x ^= x >> 17; x *= 0xed5ad4bbU; x ^= x >> 11; x *= 0xac4c1b51U;
|
| 42 |
+
x ^= x >> 15; x *= 0x31848babU; x ^= x >> 14; return x;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
// Portable reduction operators: CUDA 13's CUB dropped cub::Max()/cub::Sum().
|
| 46 |
+
struct MaxOp { __device__ __forceinline__ float operator()(const float& a, const float& b) const { return fmaxf(a, b); } };
|
| 47 |
+
|
| 48 |
+
// bit == 1 means allowed; disallowed logits become -inf. grid = (xtiles, B).
|
| 49 |
+
template <typename T>
|
| 50 |
+
__global__ void bitmask_kernel(T* logits, const int32_t* mask, int V, int mask_stride) {
|
| 51 |
+
int row = blockIdx.y;
|
| 52 |
+
const int32_t* m = mask + (long)row * mask_stride;
|
| 53 |
+
T* lg = logits + (long)row * V;
|
| 54 |
+
for (int i = blockIdx.x * blockDim.x + threadIdx.x; i < V; i += gridDim.x * blockDim.x) {
|
| 55 |
+
if (!((m[i >> 5] >> (i & 31)) & 1)) set_ninf(lg[i]);
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
// probs[row] = softmax(logits[row] / max(temp,eps)); one block per row.
|
| 60 |
+
template <typename T>
|
| 61 |
+
__global__ void softmax_temp_kernel(const T* __restrict__ logits, const float* __restrict__ temp,
|
| 62 |
+
float* __restrict__ probs, int V) {
|
| 63 |
+
int row = blockIdx.x;
|
| 64 |
+
const T* lg = logits + (long)row * V;
|
| 65 |
+
float* pr = probs + (long)row * V;
|
| 66 |
+
float t = temp[row];
|
| 67 |
+
float scale = (t > 0.f) ? (1.f / t) : 1.f;
|
| 68 |
+
|
| 69 |
+
typedef cub::BlockReduce<float, TPB> BR;
|
| 70 |
+
__shared__ typename BR::TempStorage ts;
|
| 71 |
+
__shared__ float sh;
|
| 72 |
+
|
| 73 |
+
float m = -INFINITY;
|
| 74 |
+
for (int i = threadIdx.x; i < V; i += blockDim.x) m = fmaxf(m, to_f(lg[i]));
|
| 75 |
+
float gmax = BR(ts).Reduce(m, MaxOp());
|
| 76 |
+
if (threadIdx.x == 0) sh = gmax;
|
| 77 |
+
__syncthreads();
|
| 78 |
+
gmax = sh;
|
| 79 |
+
__syncthreads();
|
| 80 |
+
|
| 81 |
+
float s = 0.f;
|
| 82 |
+
for (int i = threadIdx.x; i < V; i += blockDim.x) {
|
| 83 |
+
float e = __expf((to_f(lg[i]) - gmax) * scale);
|
| 84 |
+
pr[i] = e;
|
| 85 |
+
s += e;
|
| 86 |
+
}
|
| 87 |
+
float gsum = BR(ts).Sum(s);
|
| 88 |
+
if (threadIdx.x == 0) sh = gsum;
|
| 89 |
+
__syncthreads();
|
| 90 |
+
float inv = 1.f / sh;
|
| 91 |
+
__syncthreads();
|
| 92 |
+
for (int i = threadIdx.x; i < V; i += blockDim.x) pr[i] *= inv;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// sp/si: probabilities and token ids sorted descending per row. Compute the
|
| 96 |
+
// kept prefix length L = min(top_k, top_p, min_p cutoffs), then inverse-CDF draw.
|
| 97 |
+
__global__ void cutoff_sample_kernel(const float* __restrict__ sp, const int* __restrict__ si,
|
| 98 |
+
const float* __restrict__ top_p, const int* __restrict__ top_k,
|
| 99 |
+
const float* __restrict__ min_p, const float* __restrict__ temp,
|
| 100 |
+
unsigned seed, unsigned offset, int64_t* __restrict__ out, int V) {
|
| 101 |
+
int row = blockIdx.x;
|
| 102 |
+
if (threadIdx.x != 0) return;
|
| 103 |
+
const float* p = sp + (long)row * V;
|
| 104 |
+
const int* idx = si + (long)row * V;
|
| 105 |
+
float tp = top_p[row]; int tk = top_k[row]; float mp = min_p[row]; float t = temp[row];
|
| 106 |
+
|
| 107 |
+
long L = (tk > 0 && tk < V) ? (long)tk : (long)V; // top-k
|
| 108 |
+
if (tp > 0.f && tp < 1.f) { // top-p (nucleus)
|
| 109 |
+
float c = 0.f; long Lp = L;
|
| 110 |
+
for (long j = 0; j < L; j++) { c += p[j]; if (c >= tp) { Lp = j + 1; break; } }
|
| 111 |
+
L = Lp;
|
| 112 |
+
}
|
| 113 |
+
if (mp > 0.f) { // min-p
|
| 114 |
+
float thr = mp * p[0]; long j = 0;
|
| 115 |
+
while (j < L && p[j] >= thr) j++;
|
| 116 |
+
L = (j > 0) ? j : 1;
|
| 117 |
+
}
|
| 118 |
+
if (L < 1) L = 1;
|
| 119 |
+
if (t <= 0.f) L = 1; // greedy
|
| 120 |
+
|
| 121 |
+
float sum = 0.f;
|
| 122 |
+
for (long j = 0; j < L; j++) sum += p[j];
|
| 123 |
+
unsigned h = triple32((unsigned)row * 0x9E3779B1u ^ seed ^ (offset * 0x85EBCA6Bu));
|
| 124 |
+
float u = (h >> 8) * (1.0f / 16777216.0f); // [0, 1)
|
| 125 |
+
float target = u * sum, c = 0.f;
|
| 126 |
+
int pick = idx[L - 1];
|
| 127 |
+
for (long j = 0; j < L; j++) { c += p[j]; if (c >= target) { pick = idx[j]; break; } }
|
| 128 |
+
out[row] = pick;
|
| 129 |
+
}
|
| 130 |
+
} // namespace
|
| 131 |
+
|
| 132 |
+
void apply_token_bitmask_inplace(at::Tensor logits, at::Tensor bitmask) {
|
| 133 |
+
TORCH_CHECK(logits.is_cuda() && logits.dim() == 2 && logits.is_contiguous(),
|
| 134 |
+
"logits must be contiguous [B, V] CUDA");
|
| 135 |
+
TORCH_CHECK(bitmask.is_cuda() && bitmask.scalar_type() == at::kInt,
|
| 136 |
+
"bitmask must be int32 CUDA");
|
| 137 |
+
const at::cuda::CUDAGuard guard(logits.device());
|
| 138 |
+
int B = logits.size(0), V = logits.size(1);
|
| 139 |
+
auto bm = bitmask.contiguous();
|
| 140 |
+
int ms = bm.size(-1);
|
| 141 |
+
TORCH_CHECK(ms >= (V + 31) / 32, "bitmask has too few words for V");
|
| 142 |
+
const int32_t* m = bm.data_ptr<int32_t>();
|
| 143 |
+
int gx = (V + TPB - 1) / TPB; if (gx > 512) gx = 512; if (gx < 1) gx = 1;
|
| 144 |
+
dim3 grid(gx, B);
|
| 145 |
+
cudaStream_t s = at::cuda::getCurrentCUDAStream();
|
| 146 |
+
auto st = logits.scalar_type();
|
| 147 |
+
if (st == at::kFloat)
|
| 148 |
+
bitmask_kernel<float><<<grid, TPB, 0, s>>>(logits.data_ptr<float>(), m, V, ms);
|
| 149 |
+
else if (st == at::kHalf)
|
| 150 |
+
bitmask_kernel<__half><<<grid, TPB, 0, s>>>((__half*)logits.data_ptr(), m, V, ms);
|
| 151 |
+
else if (st == at::kBFloat16)
|
| 152 |
+
bitmask_kernel<__nv_bfloat16><<<grid, TPB, 0, s>>>((__nv_bfloat16*)logits.data_ptr(), m, V, ms);
|
| 153 |
+
else
|
| 154 |
+
TORCH_CHECK(false, "logits must be float/half/bf16");
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
at::Tensor sample(at::Tensor logits, at::Tensor temperature, at::Tensor top_p,
|
| 158 |
+
at::Tensor top_k, at::Tensor min_p, int64_t seed, int64_t offset) {
|
| 159 |
+
TORCH_CHECK(logits.is_cuda() && logits.dim() == 2 && logits.is_contiguous(),
|
| 160 |
+
"logits must be contiguous [B, V] CUDA");
|
| 161 |
+
const at::cuda::CUDAGuard guard(logits.device());
|
| 162 |
+
int B = logits.size(0), V = logits.size(1);
|
| 163 |
+
cudaStream_t s = at::cuda::getCurrentCUDAStream();
|
| 164 |
+
auto fopt = logits.options().dtype(at::kFloat);
|
| 165 |
+
auto iopt = logits.options().dtype(at::kInt);
|
| 166 |
+
|
| 167 |
+
auto tf = temperature.to(at::kFloat).contiguous();
|
| 168 |
+
auto pf = top_p.to(at::kFloat).contiguous();
|
| 169 |
+
auto kf = top_k.to(at::kInt).contiguous();
|
| 170 |
+
auto mf = min_p.to(at::kFloat).contiguous();
|
| 171 |
+
|
| 172 |
+
auto probs = at::empty({B, V}, fopt);
|
| 173 |
+
auto st = logits.scalar_type();
|
| 174 |
+
if (st == at::kFloat)
|
| 175 |
+
softmax_temp_kernel<float><<<B, TPB, 0, s>>>(logits.data_ptr<float>(), tf.data_ptr<float>(), probs.data_ptr<float>(), V);
|
| 176 |
+
else if (st == at::kHalf)
|
| 177 |
+
softmax_temp_kernel<__half><<<B, TPB, 0, s>>>((const __half*)logits.data_ptr(), tf.data_ptr<float>(), probs.data_ptr<float>(), V);
|
| 178 |
+
else if (st == at::kBFloat16)
|
| 179 |
+
softmax_temp_kernel<__nv_bfloat16><<<B, TPB, 0, s>>>((const __nv_bfloat16*)logits.data_ptr(), tf.data_ptr<float>(), probs.data_ptr<float>(), V);
|
| 180 |
+
else
|
| 181 |
+
TORCH_CHECK(false, "logits must be float/half/bf16");
|
| 182 |
+
|
| 183 |
+
auto sorted_p = at::empty({B, V}, fopt);
|
| 184 |
+
auto sorted_i = at::empty({B, V}, iopt);
|
| 185 |
+
auto idx_in = at::arange(V, iopt).unsqueeze(0).repeat({B, 1}).contiguous();
|
| 186 |
+
auto offsets = at::arange(0, (long)(B + 1) * V, V, iopt).contiguous();
|
| 187 |
+
|
| 188 |
+
size_t tmp_bytes = 0;
|
| 189 |
+
cub::DeviceSegmentedRadixSort::SortPairsDescending(
|
| 190 |
+
nullptr, tmp_bytes, probs.data_ptr<float>(), sorted_p.data_ptr<float>(),
|
| 191 |
+
idx_in.data_ptr<int>(), sorted_i.data_ptr<int>(), B * V, B,
|
| 192 |
+
offsets.data_ptr<int>(), offsets.data_ptr<int>() + 1, 0, (int)sizeof(float) * 8, s);
|
| 193 |
+
auto tmp = at::empty({(long)tmp_bytes}, logits.options().dtype(at::kByte));
|
| 194 |
+
cub::DeviceSegmentedRadixSort::SortPairsDescending(
|
| 195 |
+
tmp.data_ptr(), tmp_bytes, probs.data_ptr<float>(), sorted_p.data_ptr<float>(),
|
| 196 |
+
idx_in.data_ptr<int>(), sorted_i.data_ptr<int>(), B * V, B,
|
| 197 |
+
offsets.data_ptr<int>(), offsets.data_ptr<int>() + 1, 0, (int)sizeof(float) * 8, s);
|
| 198 |
+
|
| 199 |
+
auto out = at::empty({B}, logits.options().dtype(at::kLong));
|
| 200 |
+
cutoff_sample_kernel<<<B, 32, 0, s>>>(sorted_p.data_ptr<float>(), sorted_i.data_ptr<int>(),
|
| 201 |
+
pf.data_ptr<float>(), kf.data_ptr<int>(), mf.data_ptr<float>(), tf.data_ptr<float>(),
|
| 202 |
+
(unsigned)seed, (unsigned)offset, out.data_ptr<int64_t>(), V);
|
| 203 |
+
return out;
|
| 204 |
+
}
|
tests/test_logits_processor.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
import torch
|
| 3 |
+
|
| 4 |
+
import kernels
|
| 5 |
+
|
| 6 |
+
lp = kernels.get_kernel("phanerozoic/logits-processor", version=1, trust_remote_code=True)
|
| 7 |
+
|
| 8 |
+
requires_cuda = pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required")
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def _pack(allowed_sets, V, device):
|
| 12 |
+
nwords = (V + 31) // 32
|
| 13 |
+
mask = torch.zeros(len(allowed_sets), nwords, dtype=torch.int32, device=device)
|
| 14 |
+
for r, s in enumerate(allowed_sets):
|
| 15 |
+
for tok in s:
|
| 16 |
+
mask[r, tok >> 5] |= (1 << (tok & 31))
|
| 17 |
+
return mask
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@requires_cuda
|
| 21 |
+
@pytest.mark.parametrize("dtype", [torch.float32, torch.float16, torch.bfloat16])
|
| 22 |
+
def test_bitmask_sets_disallowed_to_neg_inf(dtype):
|
| 23 |
+
V = 130
|
| 24 |
+
logits = torch.randn(2, V, device="cuda", dtype=dtype)
|
| 25 |
+
allowed = [{3, 7, 50, 129}, {0, 42, 63}]
|
| 26 |
+
ref = logits.clone()
|
| 27 |
+
lp.apply_token_bitmask(logits, _pack(allowed, V, "cuda"))
|
| 28 |
+
for r, s in enumerate(allowed):
|
| 29 |
+
for tok in range(V):
|
| 30 |
+
if tok in s:
|
| 31 |
+
assert logits[r, tok] == ref[r, tok]
|
| 32 |
+
else:
|
| 33 |
+
assert logits[r, tok].float() == float("-inf")
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
@requires_cuda
|
| 37 |
+
def test_greedy_and_topk1_are_argmax():
|
| 38 |
+
logits = torch.randn(8, 2000, device="cuda")
|
| 39 |
+
amax = logits.argmax(-1)
|
| 40 |
+
assert torch.equal(lp.sample(logits, temperature=0.0), amax)
|
| 41 |
+
assert torch.equal(lp.sample(logits, top_k=1, temperature=1.0), amax)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@requires_cuda
|
| 45 |
+
def test_min_p_and_topp_exclude_low_prob():
|
| 46 |
+
logits = torch.full((4, 50), -10.0, device="cuda")
|
| 47 |
+
logits[:, 7] = 10.0 # one dominant token, softmax ~1.0
|
| 48 |
+
assert torch.equal(lp.sample(logits, temperature=1.0, min_p=0.5, seed=1),
|
| 49 |
+
torch.full((4,), 7, device="cuda"))
|
| 50 |
+
assert torch.equal(lp.sample(logits, temperature=1.0, top_p=0.9, seed=2),
|
| 51 |
+
torch.full((4,), 7, device="cuda"))
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
@requires_cuda
|
| 55 |
+
def test_deterministic():
|
| 56 |
+
logits = torch.randn(16, 500, device="cuda")
|
| 57 |
+
a = lp.sample(logits, temperature=1.0, seed=99, offset=3)
|
| 58 |
+
b = lp.sample(logits, temperature=1.0, seed=99, offset=3)
|
| 59 |
+
assert torch.equal(a, b)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
@requires_cuda
|
| 63 |
+
def test_distribution_matches_softmax():
|
| 64 |
+
torch.manual_seed(0)
|
| 65 |
+
V, N = 32, 40000
|
| 66 |
+
base = torch.randn(V, device="cuda")
|
| 67 |
+
logits = base.unsqueeze(0).repeat(N, 1)
|
| 68 |
+
toks = lp.sample(logits, temperature=1.0, seed=1234)
|
| 69 |
+
emp = torch.bincount(toks, minlength=V).float() / N
|
| 70 |
+
ref = torch.softmax(base, -1)
|
| 71 |
+
assert (emp - ref).abs().max().item() < 0.03
|
torch-ext/logits_processor/__init__.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""logits-processor: the generation last-mile, loadable through `kernels`.
|
| 2 |
+
|
| 3 |
+
Two fused ops that every serving stack runs on the hot path:
|
| 4 |
+
|
| 5 |
+
- ``apply_token_bitmask`` - structured / guided decoding. Set the logits of
|
| 6 |
+
disallowed tokens to -inf from a packed allow-mask (XGrammar convention:
|
| 7 |
+
bit 1 = allowed), so the next softmax cannot pick them. This is the kernel
|
| 8 |
+
behind constrained JSON and tool-calling output.
|
| 9 |
+
- ``sample`` - temperature, then the intersection of top-k / top-p (nucleus) /
|
| 10 |
+
min-p filtering, then a multinomial draw, fused into one logits -> token op
|
| 11 |
+
with a counter-based RNG that is deterministic in ``(seed, row, offset)``.
|
| 12 |
+
``temperature <= 0`` is greedy (argmax).
|
| 13 |
+
|
| 14 |
+
Both take ``[B, V]`` logits in float / half / bf16. Sampling parameters are
|
| 15 |
+
per-row: pass a scalar to broadcast, or a length-B tensor for a heterogeneous
|
| 16 |
+
batch (continuous batching, per-request grammars).
|
| 17 |
+
"""
|
| 18 |
+
from typing import Union
|
| 19 |
+
|
| 20 |
+
import torch
|
| 21 |
+
|
| 22 |
+
from ._ops import ops
|
| 23 |
+
|
| 24 |
+
__all__ = ["apply_token_bitmask", "sample"]
|
| 25 |
+
|
| 26 |
+
Param = Union[float, int, torch.Tensor]
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _row(x: Param, B: int, device, dtype) -> torch.Tensor:
|
| 30 |
+
if isinstance(x, torch.Tensor):
|
| 31 |
+
return x.to(device=device, dtype=dtype).contiguous()
|
| 32 |
+
return torch.full((B,), x, device=device, dtype=dtype)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def apply_token_bitmask(logits: torch.Tensor, bitmask: torch.Tensor) -> torch.Tensor:
|
| 36 |
+
"""In place: set disallowed tokens' logits to -inf.
|
| 37 |
+
|
| 38 |
+
logits: ``[B, V]`` (float/half/bf16). bitmask: ``[B, ceil(V/32)]`` int32,
|
| 39 |
+
bit 1 = token allowed. Returns ``logits`` for chaining.
|
| 40 |
+
"""
|
| 41 |
+
ops.apply_token_bitmask_inplace(logits, bitmask.to(torch.int32).contiguous())
|
| 42 |
+
return logits
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def sample(logits: torch.Tensor, temperature: Param = 1.0, top_k: Param = 0,
|
| 46 |
+
top_p: Param = 1.0, min_p: Param = 0.0, seed: int = 0,
|
| 47 |
+
offset: int = 0) -> torch.Tensor:
|
| 48 |
+
"""Fused temperature + top-k ∩ top-p ∩ min-p + multinomial sample.
|
| 49 |
+
|
| 50 |
+
top_k <= 0, top_p >= 1, and min_p <= 0 each disable that filter.
|
| 51 |
+
temperature <= 0 selects the argmax. Returns int64 token ids ``[B]``.
|
| 52 |
+
Same ``(seed, offset)`` and logits give the same tokens.
|
| 53 |
+
"""
|
| 54 |
+
assert logits.dim() == 2, "logits must be [B, V]"
|
| 55 |
+
B, dev = logits.size(0), logits.device
|
| 56 |
+
return ops.sample(
|
| 57 |
+
logits.contiguous(),
|
| 58 |
+
_row(temperature, B, dev, torch.float32),
|
| 59 |
+
_row(top_p, B, dev, torch.float32),
|
| 60 |
+
_row(top_k, B, dev, torch.int32),
|
| 61 |
+
_row(min_p, B, dev, torch.float32),
|
| 62 |
+
int(seed), int(offset),
|
| 63 |
+
)
|
torch-ext/torch_binding.cpp
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#include <torch/library.h>
|
| 2 |
+
|
| 3 |
+
#include "registration.h"
|
| 4 |
+
#include "torch_binding.h"
|
| 5 |
+
|
| 6 |
+
TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
|
| 7 |
+
ops.def("apply_token_bitmask_inplace(Tensor(a!) logits, Tensor bitmask) -> ()");
|
| 8 |
+
ops.def("sample(Tensor logits, Tensor temperature, Tensor top_p, Tensor top_k, "
|
| 9 |
+
"Tensor min_p, int seed, int offset) -> Tensor");
|
| 10 |
+
|
| 11 |
+
#if defined(CUDA_KERNEL) || defined(ROCM_KERNEL)
|
| 12 |
+
ops.impl("apply_token_bitmask_inplace", torch::kCUDA, &apply_token_bitmask_inplace);
|
| 13 |
+
ops.impl("sample", torch::kCUDA, &sample);
|
| 14 |
+
#endif
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
REGISTER_EXTENSION(TORCH_EXTENSION_NAME)
|
torch-ext/torch_binding.h
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#pragma once
|
| 2 |
+
|
| 3 |
+
#include <torch/torch.h>
|
| 4 |
+
|
| 5 |
+
// Set logits of disallowed tokens to -inf, in place. bitmask is packed int32,
|
| 6 |
+
// bit == 1 means the token is allowed (XGrammar convention); one row of
|
| 7 |
+
// ceil(V/32) words per batch element. logits: [B, V], bitmask: [B, ceil(V/32)].
|
| 8 |
+
void apply_token_bitmask_inplace(at::Tensor logits, at::Tensor bitmask);
|
| 9 |
+
|
| 10 |
+
// Fused logits -> token sampler. Applies per-row temperature, then the
|
| 11 |
+
// intersection of top-k / top-p (nucleus) / min-p filtering, renormalizes the
|
| 12 |
+
// surviving set, and draws one token per row with a counter-based RNG keyed by
|
| 13 |
+
// (seed, row, offset). temperature <= 0 is greedy (argmax). logits: [B, V]
|
| 14 |
+
// (float/half/bf16); temperature/top_p/min_p: [B] float; top_k: [B] int32.
|
| 15 |
+
// Returns tokens: [B] int64.
|
| 16 |
+
at::Tensor sample(at::Tensor logits, at::Tensor temperature, at::Tensor top_p,
|
| 17 |
+
at::Tensor top_k, at::Tensor min_p, int64_t seed, int64_t offset);
|