CharlesCNorton commited on
Commit
9aa33e7
·
1 Parent(s): 726f31a

Add logits-processor source, tests, and model card

Browse files
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);