Skip to content

Commit 03b6e9d

Browse files
[executorch][muse-glimmer] Add exportable DFlash token sampling (pytorch#23144)
This PR was created by the merge bot to help merge the original PR into the main branch. ghstack PR number: pytorch#23085 by @Gasoonjia ^ Please use this as the source of truth for the PR details, comments, and reviews ghstack PR base: https://github.com/pytorch/executorch/tree/gh/Gasoonjia/29/base ghstack PR head: https://github.com/pytorch/executorch/tree/gh/Gasoonjia/29/head Merge bot PR base: https://github.com/pytorch/executorch/tree/gh/Gasoonjia/28/orig Merge bot PR head: https://github.com/pytorch/executorch/tree/gh/Gasoonjia/29/orig Differential Revision: [D119425587](https://our.internmc.facebook.com/intern/diff/D119425587/) @diff-train-skip-merge --------- Co-authored-by: gasoonjia <gasoonjia@icloud.com>
1 parent cb94e69 commit 03b6e9d

6 files changed

Lines changed: 727 additions & 7 deletions

File tree

‎backends/cuda/runtime/shims/sort.cu‎

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
#include <thrust/execution_policy.h>
1515
#include <thrust/sort.h>
1616

17+
#include <new>
18+
1719
#include <executorch/backends/aoti/utils.h>
1820
#include <executorch/backends/cuda/runtime/shims/memory.h>
1921
#include <executorch/backends/cuda/runtime/shims/sort.h>
@@ -108,6 +110,27 @@ void launch_permute(
108110
}
109111
}
110112

113+
// Stream-ordered scratch for thrust. With par_nosync this keeps each slice sort
114+
// from blocking on cudaMalloc/cudaFree and synchronizing the stream.
115+
struct StreamOrderedAllocator {
116+
using value_type = char;
117+
118+
char* allocate(std::ptrdiff_t bytes) {
119+
void* ptr = nullptr;
120+
if (cudaMallocAsync(&ptr, static_cast<size_t>(bytes), stream) !=
121+
cudaSuccess) {
122+
throw std::bad_alloc();
123+
}
124+
return static_cast<char*>(ptr);
125+
}
126+
127+
void deallocate(char* ptr, size_t) {
128+
(void)cudaFreeAsync(ptr, stream);
129+
}
130+
131+
cudaStream_t stream;
132+
};
133+
111134
template <typename T>
112135
void sort_slice_impl(
113136
T* keys,
@@ -118,17 +141,16 @@ void sort_slice_impl(
118141
cudaStream_t stream) {
119142
auto k = thrust::device_pointer_cast(keys);
120143
auto v = thrust::device_pointer_cast(values);
144+
StreamOrderedAllocator allocator{stream};
145+
const auto policy = thrust::cuda::par_nosync(allocator).on(stream);
121146
if (stable && descending) {
122-
thrust::stable_sort_by_key(
123-
thrust::cuda::par.on(stream), k, k + n, v, thrust::greater<T>());
147+
thrust::stable_sort_by_key(policy, k, k + n, v, thrust::greater<T>());
124148
} else if (stable) {
125-
thrust::stable_sort_by_key(
126-
thrust::cuda::par.on(stream), k, k + n, v);
149+
thrust::stable_sort_by_key(policy, k, k + n, v);
127150
} else if (descending) {
128-
thrust::sort_by_key(
129-
thrust::cuda::par.on(stream), k, k + n, v, thrust::greater<T>());
151+
thrust::sort_by_key(policy, k, k + n, v, thrust::greater<T>());
130152
} else {
131-
thrust::sort_by_key(thrust::cuda::par.on(stream), k, k + n, v);
153+
thrust::sort_by_key(policy, k, k + n, v);
132154
}
133155
}
134156

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
load("@fbcode_macros//build_defs:build_file_migration.bzl", "fbcode_target")
2+
load("@fbsource//xplat/executorch/build:runtime_wrapper.bzl", "runtime")
3+
4+
oncall("executorch")
5+
6+
fbcode_target(
7+
_kind = runtime.python_library,
8+
name = "dflash_token_sampler",
9+
srcs = ["dflash_token_sampler.py"],
10+
_is_external_target = True,
11+
base_module = "executorch.examples.models.muse_glimmer.model",
12+
visibility = ["PUBLIC"],
13+
deps = [
14+
"//caffe2:torch",
15+
],
16+
)
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
"""Exportable DFlash sampling.
8+
9+
The two modules below are exported as CUDA DFlash methods so draft sampling,
10+
speculative verification, and correction sampling run on the device. They
11+
mirror the host sampler in ``runtime/engine/sampling.h``: stable top-k,
12+
top-p over the sorted distribution, inverse-CDF sampling in original token
13+
order, and exact argmax at temperature 0. Uniforms are drawn in-graph; the
14+
pure functions take them explicitly so tests can pin them.
15+
"""
16+
17+
from __future__ import annotations
18+
19+
import torch
20+
from torch import nn
21+
22+
23+
def sampling_probabilities(
24+
logits: torch.Tensor,
25+
temperature: torch.Tensor,
26+
top_k: torch.Tensor,
27+
top_p: torch.Tensor,
28+
) -> tuple[torch.Tensor, torch.Tensor]:
29+
"""Returns ``[R, V]`` filtered probabilities and ``[R]`` argmax tokens."""
30+
vocab_size = logits.shape[-1]
31+
logits = logits.float()
32+
greedy_tokens = torch.argmax(logits, dim=-1)
33+
34+
safe_temperature = torch.where(
35+
temperature > 0.0, temperature, torch.ones_like(temperature)
36+
)
37+
sorted_logits, sorted_indices = torch.sort(
38+
logits / safe_temperature, dim=-1, descending=True, stable=True
39+
)
40+
ranks = torch.arange(vocab_size, device=logits.device)
41+
effective_top_k = torch.where(
42+
(top_k > 0) & (top_k < vocab_size), top_k, torch.full_like(top_k, vocab_size)
43+
)
44+
sorted_logits = torch.where(
45+
ranks < effective_top_k,
46+
sorted_logits,
47+
torch.full_like(sorted_logits, float("-inf")),
48+
)
49+
sorted_probabilities = torch.softmax(sorted_logits, dim=-1)
50+
51+
mass_before_token = (
52+
torch.cumsum(sorted_probabilities, dim=-1) - sorted_probabilities
53+
)
54+
top_p_enabled = (top_p > 0.0) & (top_p < 1.0)
55+
sorted_probabilities = torch.where(
56+
~top_p_enabled | (mass_before_token < top_p),
57+
sorted_probabilities,
58+
torch.zeros_like(sorted_probabilities),
59+
)
60+
sorted_probabilities = sorted_probabilities / sorted_probabilities.sum(
61+
dim=-1, keepdim=True
62+
)
63+
probabilities = torch.zeros_like(sorted_probabilities).scatter(
64+
-1, sorted_indices, sorted_probabilities
65+
)
66+
67+
greedy_probabilities = torch.nn.functional.one_hot(
68+
greedy_tokens, num_classes=vocab_size
69+
).to(probabilities.dtype)
70+
probabilities = torch.where(temperature > 0.0, probabilities, greedy_probabilities)
71+
return probabilities, greedy_tokens
72+
73+
74+
def categorical_sample(
75+
probabilities: torch.Tensor, uniforms: torch.Tensor
76+
) -> torch.Tensor:
77+
"""First token whose inclusive CDF reaches the row's uniform."""
78+
cumulative = torch.cumsum(probabilities, dim=-1)
79+
return torch.sum(
80+
cumulative < uniforms.unsqueeze(-1), dim=-1, dtype=torch.int64
81+
).clamp(max=probabilities.shape[-1] - 1)
82+
83+
84+
def strict_categorical_sample(
85+
probabilities: torch.Tensor, uniforms: torch.Tensor
86+
) -> torch.Tensor:
87+
"""First supported token whose CDF exceeds the uniform, else the last one."""
88+
vocab_size = probabilities.shape[-1]
89+
supported = probabilities > 0.0
90+
token_ids = torch.arange(vocab_size, device=probabilities.device)
91+
crossing = torch.where(
92+
supported & (torch.cumsum(probabilities, dim=-1) > uniforms.unsqueeze(-1)),
93+
token_ids,
94+
torch.full_like(token_ids, vocab_size),
95+
).amin(dim=-1)
96+
last_supported = torch.where(
97+
supported, token_ids, torch.zeros_like(token_ids)
98+
).amax(dim=-1)
99+
return torch.where(crossing < vocab_size, crossing, last_supported)
100+
101+
102+
def sample_tokens(
103+
logits: torch.Tensor,
104+
temperature: torch.Tensor,
105+
top_k: torch.Tensor,
106+
top_p: torch.Tensor,
107+
uniforms: torch.Tensor,
108+
) -> tuple[torch.Tensor, torch.Tensor]:
109+
probabilities, greedy_tokens = sampling_probabilities(
110+
logits, temperature, top_k, top_p
111+
)
112+
tokens = torch.where(
113+
temperature > 0.0, categorical_sample(probabilities, uniforms), greedy_tokens
114+
)
115+
return tokens, probabilities
116+
117+
118+
def verify_speculative(
119+
target_logits: torch.Tensor,
120+
draft_probabilities: torch.Tensor,
121+
candidates: torch.Tensor,
122+
temperature: torch.Tensor,
123+
top_k: torch.Tensor,
124+
top_p: torch.Tensor,
125+
draft_argmax: torch.Tensor,
126+
accept_uniforms: torch.Tensor,
127+
correction_uniform: torch.Tensor,
128+
) -> torch.Tensor:
129+
"""Verifies ``R`` proposals against ``R + 1`` target rows.
130+
131+
Returns ``[committed_count, correction_token, *candidates]`` so the host
132+
reads one cycle result with a single copy.
133+
"""
134+
num_proposals = draft_probabilities.shape[0]
135+
target_probabilities, target_greedy = sampling_probabilities(
136+
target_logits, temperature, top_k, top_p
137+
)
138+
proposals = candidates[1:]
139+
proposal_index = proposals.unsqueeze(-1)
140+
p = target_probabilities[:num_proposals].gather(-1, proposal_index).squeeze(-1)
141+
q = torch.where(
142+
draft_argmax,
143+
torch.ones_like(p),
144+
draft_probabilities.gather(-1, proposal_index).squeeze(-1),
145+
)
146+
acceptance = torch.where(q > 0.0, torch.clamp(p / q, max=1.0), torch.ones_like(p))
147+
accepted = torch.where(
148+
temperature > 0.0,
149+
accept_uniforms < acceptance,
150+
target_greedy[:num_proposals] == proposals,
151+
)
152+
153+
rows = torch.arange(num_proposals, device=candidates.device)
154+
# Row of the first rejection, or num_proposals (the bonus row) if none.
155+
decision_row = (
156+
torch.where(accepted, torch.full_like(rows, num_proposals), rows)
157+
.amin()
158+
.reshape(1)
159+
)
160+
rejected = decision_row < num_proposals
161+
proposal_row = decision_row.clamp(max=num_proposals - 1)
162+
163+
row_target = target_probabilities.index_select(0, decision_row).squeeze(0)
164+
row_draft = draft_probabilities.index_select(0, proposal_row).squeeze(0)
165+
166+
excluded = row_target.scatter(0, proposals.index_select(0, proposal_row), 0.0)
167+
excluded_sum = excluded.sum()
168+
excluded = excluded / torch.where(
169+
excluded_sum > 0.0, excluded_sum, torch.ones_like(excluded_sum)
170+
)
171+
residual = torch.clamp_min(row_target - row_draft, 0.0)
172+
residual_sum = residual.sum()
173+
residual = torch.where(
174+
residual_sum > 0.0,
175+
residual
176+
/ torch.where(residual_sum > 0.0, residual_sum, torch.ones_like(residual_sum)),
177+
row_target,
178+
)
179+
180+
rejection_correction = torch.where(
181+
draft_argmax,
182+
strict_categorical_sample(excluded.unsqueeze(0), correction_uniform),
183+
categorical_sample(residual.unsqueeze(0), correction_uniform),
184+
)
185+
stochastic_correction = torch.where(
186+
rejected,
187+
rejection_correction,
188+
categorical_sample(row_target.unsqueeze(0), correction_uniform),
189+
)
190+
correction = torch.where(
191+
temperature > 0.0,
192+
stochastic_correction,
193+
target_greedy.index_select(0, decision_row),
194+
)
195+
return torch.cat([decision_row + 1, correction, candidates])
196+
197+
198+
class DFlashTokenSampler(nn.Module):
199+
"""Samples ``[R, V]`` logits; returns ``[R]`` tokens and ``[R, V]`` probs."""
200+
201+
def forward(
202+
self,
203+
logits: torch.Tensor,
204+
temperature: torch.Tensor,
205+
top_k: torch.Tensor,
206+
top_p: torch.Tensor,
207+
) -> tuple[torch.Tensor, torch.Tensor]:
208+
uniforms = torch.rand_like(logits[:, 0], dtype=torch.float32)
209+
return sample_tokens(logits, temperature, top_k, top_p, uniforms)
210+
211+
212+
class DFlashSpeculativeVerifier(nn.Module):
213+
"""Verifies ``R`` proposals; see :func:`verify_speculative`."""
214+
215+
def forward(
216+
self,
217+
target_logits: torch.Tensor,
218+
draft_probabilities: torch.Tensor,
219+
candidates: torch.Tensor,
220+
temperature: torch.Tensor,
221+
top_k: torch.Tensor,
222+
top_p: torch.Tensor,
223+
draft_argmax: torch.Tensor,
224+
) -> torch.Tensor:
225+
accept_uniforms = torch.rand_like(draft_probabilities[:, 0])
226+
correction_uniform = torch.rand_like(target_logits[:1, 0], dtype=torch.float32)
227+
return verify_speculative(
228+
target_logits,
229+
draft_probabilities,
230+
candidates,
231+
temperature,
232+
top_k,
233+
top_p,
234+
draft_argmax,
235+
accept_uniforms,
236+
correction_uniform,
237+
)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
load("@fbcode_macros//build_defs:build_file_migration.bzl", "fbcode_target")
2+
load("@fbcode_macros//build_defs:python_unittest.bzl", "python_unittest")
3+
4+
oncall("executorch")
5+
6+
fbcode_target(
7+
_kind = python_unittest,
8+
name = "dflash_speculative_verifier_test",
9+
srcs = ["test_dflash_speculative_verifier.py"],
10+
deps = [
11+
"//caffe2:torch",
12+
"//executorch/examples/models/muse-glimmer/model:dflash_token_sampler",
13+
],
14+
)
15+
16+
fbcode_target(
17+
_kind = python_unittest,
18+
name = "dflash_token_sampler_test",
19+
srcs = ["test_dflash_token_sampler.py"],
20+
deps = [
21+
"//caffe2:torch",
22+
"//executorch/examples/models/muse-glimmer/model:dflash_token_sampler",
23+
],
24+
)

0 commit comments

Comments
 (0)