Skip to content

Commit 8d9eec3

Browse files
[executorch][muse] Run the CUDA DFlash drafter in CUDA graphs (#24043)
This PR was created by the merge bot to help merge the original PR into the main branch. ghstack PR number: #23985 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/84/base ghstack PR head: https://github.com/pytorch/executorch/tree/gh/Gasoonjia/84/head Merge bot PR base: https://github.com/pytorch/executorch/tree/gh/Gasoonjia/83/orig Merge bot PR head: https://github.com/pytorch/executorch/tree/gh/Gasoonjia/84/orig Differential Revision: [D125705719](https://our.internmc.facebook.com/intern/diff/D125705719/) @diff-train-skip-merge Co-authored-by: gasoonjia <gasoonjia@icloud.com>
1 parent c8ce66c commit 8d9eec3

10 files changed

Lines changed: 296 additions & 18 deletions

File tree

‎backends/cuda/batching/cuda_executor.cpp‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,9 @@ Error set_backend_options(
198198
}
199199
}
200200
}
201+
for (const std::string& name : options.additional_cuda_graph_methods) {
202+
graph_methods += (graph_methods.empty() ? "" : ",") + name;
203+
}
201204
ET_CHECK_OR_RETURN_ERROR(
202205
graph_methods.size() < ::executorch::runtime::kMaxOptionValueLength,
203206
InvalidArgument,
@@ -288,7 +291,7 @@ Result<std::unique_ptr<CudaExecutor>> CudaExecutor::create(
288291
config.max_session_tokens = max_session_tokens;
289292
config.kv_dtype = kv_dtype;
290293
config.initial_capacity = initial_capacity;
291-
config.options = options;
294+
config.options = std::move(options);
292295
return create(std::move(module), config);
293296
}
294297

‎backends/cuda/batching/cuda_executor.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ struct CudaExecutorOptions {
6565
// Capture every static method into a CUDA graph. forward_others stays
6666
// eager: its width varies.
6767
bool cuda_graph_for_static_methods = true;
68+
// Other static methods of the program to capture, e.g. a model adapter's.
69+
std::vector<std::string> additional_cuda_graph_methods;
6870
};
6971

7072
struct CudaExecutorConfig {

‎backends/cuda/runtime/cuda_kv_cache.cpp‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -451,10 +451,11 @@ class CudaCellCache final : public cache::CellCache, public CudaKVCache {
451451
"offgraph_kv: a %lld-token step is outside [1, %d]",
452452
static_cast<long long>(write_length),
453453
max_write_);
454+
// A static draft program pads its tail with scratch rows, as a static
455+
// target forward does; every query row stays a declared block row.
454456
const int tokens = static_cast<int>(step_seq_ids_.size());
455457
ET_CHECK_OR_RETURN_ERROR(
456-
tokens > 0 && tokens <= width &&
457-
(!dflash_block_size_ || tokens == width),
458+
tokens > 0 && tokens <= width,
458459
InvalidState,
459460
"offgraph_kv: a %d-token program cannot run %d declared tokens",
460461
width,

‎backends/cuda/runtime/test/test_cuda_kv_cache.cpp‎

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1927,7 +1927,7 @@ TEST_F(CudaCellCacheTest, DFlashUploadsKVWideCellsAndQueryWideOwnerMasks) {
19271927

19281928
TEST_F(
19291929
CudaCellCacheTest,
1930-
DFlashRejectsGroupingAndPaddedTokenCountsBeforeAllocation) {
1930+
DFlashRejectsGroupingAndNarrowProgramsBeforeAllocation) {
19311931
auto cache_ptr = make(4, 2);
19321932
auto* control = cache_ptr->as<cache::BatchControl>();
19331933
auto* kv = cache_ptr->as<cu::CudaKVCache>();
@@ -1938,7 +1938,6 @@ TEST_F(
19381938
EXPECT_FALSE(control->declare_step({a, a, b, b, a, a}));
19391939
EXPECT_FALSE(control->declare_step({a, a, b}));
19401940
ASSERT_TRUE(control->declare_step({b, b, a, a}));
1941-
EXPECT_EQ(kv->prepare_step(5, cudaStreamPerThread), Error::InvalidState);
19421941
EXPECT_EQ(kv->prepare_step(3, cudaStreamPerThread), Error::InvalidState);
19431942
EXPECT_EQ(kv->metrics().allocated_bytes, 0);
19441943
EXPECT_EQ(control->pos(a), 0);
@@ -1977,6 +1976,41 @@ TEST_F(
19771976
kv->forget_handle(&handle);
19781977
}
19791978

1979+
TEST_F(CudaCellCacheTest, DFlashStaticProgramPadsTailIntoScratch) {
1980+
auto cache_ptr = make(4, 2);
1981+
ASSERT_NE(cache_ptr, nullptr);
1982+
auto* control = cache_ptr->as<cache::BatchControl>();
1983+
auto* kv = cache_ptr->as<cu::CudaKVCache>();
1984+
auto fake = container();
1985+
auto handle = make_handle(fake);
1986+
ASSERT_TRUE(kv->note_handle(&handle).get());
1987+
const int32_t a = *control->seq_new(), b = *control->seq_new();
1988+
// One context row for a, none for b, padded to an eight-row program.
1989+
ASSERT_TRUE(control->declare_step({a, a, a, b, b}));
1990+
ASSERT_EQ(kv->prepare_step(8, cudaStreamPerThread), Error::Ok);
1991+
ASSERT_EQ(kv->rebind_for_execute(&handle), Error::Ok);
1992+
const auto cells = read_longs(fake.bound["cells"].data, 8);
1993+
EXPECT_EQ(
1994+
std::vector<int64_t>(cells.begin(), cells.begin() + 5),
1995+
std::vector<int64_t>({0, 1, 2, 3, 4}));
1996+
for (int row = 5; row < 8; ++row) {
1997+
EXPECT_EQ(cells[row], cells[7]) << row;
1998+
EXPECT_GE(cells[row], 5) << row;
1999+
}
2000+
using Mask = std::vector<std::vector<int>>;
2001+
EXPECT_EQ(
2002+
read_mask(fake.bound["mask0"].data, 4, 5),
2003+
Mask(
2004+
{{1, 1, 1, 0, 0},
2005+
{1, 1, 1, 0, 0},
2006+
{0, 0, 0, 1, 1},
2007+
{0, 0, 0, 1, 1}}));
2008+
ASSERT_EQ(kv->commit_step(8), Error::Ok);
2009+
EXPECT_EQ(control->pos(a), 1);
2010+
EXPECT_EQ(control->pos(b), 0);
2011+
kv->forget_handle(&handle);
2012+
}
2013+
19802014
TEST_F(CudaCellCacheTest, DFlashWarmupCommitsAllContextWithOneQueryPerOwner) {
19812015
auto cache_ptr = make(4, 0);
19822016
auto* control = cache_ptr->as<cache::BatchControl>();

‎examples/models/muse-glimmer/export/export_dflash.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ def _export_cuda_sampler_methods(
9595
}
9696

9797

98+
STATIC_DRAFT_PREFIX = "draft_forward_"
9899
VERIFY_METHOD = "dflash_verify"
99100
VERIFY_ARGMAX_METHOD = "dflash_verify_argmax"
100101

@@ -382,6 +383,28 @@ def prepare_cuda_packed_draft(
382383
strict=True,
383384
)
384385
programs = {"draft_forward": program}
386+
# Static decode drafts for CUDA graphs: N owners, each with at most one
387+
# block of fresh context. The runtime pads unused context rows into the
388+
# cache's scratch cell, as forward_N pads unused tokens.
389+
with torch.no_grad():
390+
for count in range(1, max_owners + 1):
391+
width = count * block
392+
# Context and queries write 2 * width K/V rows into one step.
393+
if 2 * width > max_write:
394+
break
395+
programs[f"{STATIC_DRAFT_PREFIX}{count}"] = export(
396+
combined,
397+
(
398+
torch.zeros((count, block), dtype=torch.long),
399+
torch.zeros(
400+
(1, width, config.n_target_layers * config.dim),
401+
dtype=torch.bfloat16,
402+
),
403+
torch.arange(2 * width, dtype=torch.long),
404+
torch.arange(2 * width, dtype=torch.long),
405+
),
406+
strict=True,
407+
)
385408
geometry = offgraph_kv_cache_geometry(draft_model)
386409
metadata = {
387410
key.replace("get_", "get_draft_", 1): value for key, value in geometry.items()

‎examples/models/muse-glimmer/runtime/cuda_dflash_executor.cpp‎

Lines changed: 96 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,41 @@ std::optional<DraftPlan> pack_draft(
143143
return plan;
144144
}
145145

146+
bool pad_to_static(
147+
DraftPlan& plan,
148+
int owners,
149+
int block,
150+
std::size_t row_bytes) {
151+
const int width = owners * block;
152+
const int queries = width;
153+
if (row_bytes == 0 || plan.hidden.size() % row_bytes != 0) {
154+
return false;
155+
}
156+
const int context = static_cast<int>(plan.hidden.size() / row_bytes);
157+
if (context > width ||
158+
plan.positions.size() != static_cast<std::size_t>(context + queries) ||
159+
plan.kv_order.size() != plan.positions.size()) {
160+
return false;
161+
}
162+
for (const int count : plan.counts) {
163+
if (count > block) {
164+
return false;
165+
}
166+
}
167+
const int pad = width - context;
168+
plan.hidden.resize(static_cast<std::size_t>(width) * row_bytes, 0);
169+
plan.positions.insert(plan.positions.begin() + context, pad, 0);
170+
for (auto& index : plan.kv_order) {
171+
if (index >= context) {
172+
index += pad;
173+
}
174+
}
175+
for (int row = context; row < width; ++row) {
176+
plan.kv_order.push_back(row);
177+
}
178+
return true;
179+
}
180+
146181
DeviceStaging::~DeviceStaging() {
147182
if (device_) {
148183
(void)cudaFree(device_);
@@ -443,6 +478,10 @@ Result<MuseGlimmerBackend> MuseGlimmerCudaDFlashExecutor::create(
443478
}
444479
target_config.max_sessions = config.max_sessions;
445480
target_config.max_session_tokens = config.max_session_tokens;
481+
for (int n = 1; n <= df::kOwnerLimit; ++n) {
482+
target_config.options.additional_cuda_graph_methods.push_back(
483+
df::kStaticForwardPrefix + std::to_string(n));
484+
}
446485
ET_ASSIGN_OR_RETURN(target, Target::create(target_config));
447486
auto& module = target->module();
448487
ET_ASSIGN_OR_RETURN(names, module.method_names());
@@ -629,6 +668,43 @@ bool MuseGlimmerCudaDFlashExecutor::initialize() {
629668
"CUDA DFlash: could not initialize distinct target/draft caches");
630669
return false;
631670
}
671+
const auto names = module.method_names();
672+
if (!names.ok()) {
673+
ET_LOG(Error, "CUDA DFlash: could not list the program's methods");
674+
return false;
675+
}
676+
for (int n = 1; n <= max_owners_; ++n) {
677+
const auto name = df::kStaticForwardPrefix + std::to_string(n);
678+
if (!names->count(name)) {
679+
continue;
680+
}
681+
const auto meta = module.method_meta(name);
682+
const int width = n * block_;
683+
auto shaped = [&](std::size_t index, bool input, std::vector<int> sizes) {
684+
auto tensor = input ? meta->input_tensor_meta(index)
685+
: meta->output_tensor_meta(index);
686+
return tensor.ok() &&
687+
std::equal(
688+
tensor->sizes().begin(),
689+
tensor->sizes().end(),
690+
sizes.begin(),
691+
sizes.end());
692+
};
693+
if (!meta.ok() || meta->num_inputs() != 4 || meta->num_outputs() != 1 ||
694+
!shaped(0, true, {n, block_}) ||
695+
!shaped(1, true, {1, width, hidden_width_}) ||
696+
!shaped(2, true, {2 * width}) || !shaped(3, true, {2 * width}) ||
697+
!shaped(0, false, {1, width, target_->vocab_size()}) ||
698+
batching::load_method_with_cache(
699+
module,
700+
name,
701+
::executorch::backends::cuda::kCudaBackendId,
702+
draft_install_) != Error::Ok) {
703+
ET_LOG(Error, "CUDA DFlash: invalid static draft %s", name.c_str());
704+
return false;
705+
}
706+
static_drafts_[n] = true;
707+
}
632708
initialized_ = true;
633709
return true;
634710
}
@@ -1022,23 +1098,39 @@ bool MuseGlimmerCudaDFlashExecutor::draft(
10221098
}
10231099
const int q = static_cast<int>(owners.size()) * block_;
10241100
const int c = static_cast<int>(plan->hidden.size() / row_bytes_);
1025-
const int k = c + q;
1101+
// Decode contexts fit one block per owner; take the static, graph-captured
1102+
// draft. Longer contexts drain through the dynamic one.
1103+
const bool use_static = proposals && static_drafts_[owners.size()] &&
1104+
std::all_of(plan->counts.begin(), plan->counts.end(), [this](int n) {
1105+
return n <= block_;
1106+
});
1107+
if (use_static &&
1108+
!df::pad_to_static(
1109+
*plan, static_cast<int>(owners.size()), block_, row_bytes_)) {
1110+
return false;
1111+
}
1112+
const int padded = static_cast<int>(plan->hidden.size() / row_bytes_);
1113+
const int k = padded + q;
10261114
// Host-owned inputs are staged by the export's normal H2D boundary. Keep
10271115
// their backing alive through the forward and its stream-fenced argmax.
10281116
auto tokens = make_tensor_ptr(
10291117
{static_cast<int>(owners.size()), block_}, std::move(plan->tokens));
10301118
auto hidden = make_tensor_ptr(
1031-
{1, c, hidden_width_},
1119+
{1, padded, hidden_width_},
10321120
std::move(plan->hidden),
10331121
aten::ScalarType::BFloat16);
10341122
auto positions = make_tensor_ptr({k}, std::move(plan->positions));
10351123
auto order = make_tensor_ptr({k}, std::move(plan->kv_order));
1036-
auto result = target_->module().execute(
1037-
df::kForward, {tokens, hidden, positions, order});
1124+
const std::string method = use_static
1125+
? df::kStaticForwardPrefix + std::to_string(owners.size())
1126+
: std::string(df::kForward);
1127+
auto result =
1128+
target_->module().execute(method, {tokens, hidden, positions, order});
10381129
if (!result.ok() || result->size() != 1 || !result->at(0).isTensor()) {
10391130
return false;
10401131
}
10411132
++stats_.draft_forwards;
1133+
stats_.static_draft_forwards += use_static;
10421134
const auto logits = result->at(0).toTensor();
10431135
if (logits.dim() < 2 || logits.scalar_type() != aten::ScalarType::Float ||
10441136
!dense(logits) ||

‎examples/models/muse-glimmer/runtime/cuda_dflash_executor.h‎

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <executorch/backends/cuda/batching/cuda_executor.h>
1111
#include <executorch/examples/models/muse-glimmer/runtime/backend_factory.h>
1212

13+
#include <array>
1314
#include <unordered_map>
1415

1516
namespace executorch::extension::llm {
@@ -24,6 +25,8 @@ inline constexpr char kTargetLayers[] = "get_n_target_layers";
2425
inline constexpr char kMaxOwners[] = "get_draft_max_owners";
2526
inline constexpr char kMaxWrite[] = "get_draft_max_step_tokens";
2627
inline constexpr char kMaxCells[] = "get_draft_offgraph_kv_max_cells";
28+
// Static draft_forward_{N}: N owners, each with at most one block of context.
29+
inline constexpr char kStaticForwardPrefix[] = "draft_forward_";
2730
// On-device verification of [owners, width] candidates; see
2831
// model/dflash_token_sampler.py.
2932
inline constexpr char kVerify[] = "dflash_verify";
@@ -104,6 +107,14 @@ std::optional<DraftPlan> pack_draft(
104107
int context_limit,
105108
int max_context,
106109
std::size_t row_bytes);
110+
// Widens a plan whose context fits one block per owner to the static
111+
// draft_forward_{N} shape: unused context rows go last, after every block, so
112+
// the cache writes them to its scratch cell and no query attends them.
113+
bool pad_to_static(
114+
DraftPlan& plan,
115+
int owners,
116+
int block,
117+
std::size_t row_bytes);
107118

108119
// Reusable device memory for small host-built inputs.
109120
class DeviceStaging {
@@ -129,6 +140,7 @@ class DeviceStaging {
129140
struct CudaDFlashStats {
130141
std::uint64_t executions = 0;
131142
std::uint64_t draft_forwards = 0;
143+
std::uint64_t static_draft_forwards = 0;
132144
std::uint64_t warmed_context_tokens = 0;
133145
// Calls are packed execute_raw attempts; forwards count slice callbacks.
134146
std::uint64_t pending_decode_batches = 0;
@@ -148,9 +160,11 @@ struct CudaDFlashStats {
148160
std::uint64_t failed_batches = 0;
149161
};
150162

151-
// Text-only model adapter over the existing CUDA target executor. Drafts are
152-
// greedy; dflash_verify accepts or resamples them under each session's sampling
153-
// on the device, so only token IDs return to the host.
163+
// Text-only model adapter over the existing CUDA target executor. Decode drafts
164+
// run the static draft_forward_{N} in CUDA graphs, as the target's static
165+
// methods do; longer contexts drain through the dynamic draft_forward. Drafts
166+
// are greedy; dflash_verify accepts or resamples them under each session's
167+
// sampling on the device, so only token IDs return to the host.
154168
class MuseGlimmerCudaDFlashExecutor final : public batching::Executor {
155169
public:
156170
static runtime::Result<MuseGlimmerBackend> create(
@@ -253,6 +267,8 @@ class MuseGlimmerCudaDFlashExecutor final : public batching::Executor {
253267
CudaDFlashStats stats_;
254268
cuda_dflash_detail::HiddenSnapshot verification_hidden_;
255269
cuda_dflash_detail::DeviceStaging verify_inputs_;
270+
// static_drafts_[n]: the program has draft_forward_{n}.
271+
std::array<bool, cuda_dflash_detail::kOwnerLimit + 1> static_drafts_{};
256272
bool initialized_ = false;
257273
// A failed co-batch invalidates every draft owner. Only model restart
258274
// recovers.

‎examples/models/muse-glimmer/tests/cuda_dflash_executor_test.cpp‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,36 @@ TEST(CudaDFlash, PositionalUniformsAreSeededAndIndependentOfBatching) {
6161
EXPECT_NE(u, df::positional_uniform(7, 100, 3));
6262
}
6363

64+
TEST(CudaDFlash, StaticPaddingPutsUnusedContextAfterEveryBlock) {
65+
const std::vector<std::uint8_t> a{1, 1, 2, 2}, b{3, 3};
66+
auto plan =
67+
df::pack_draft({{9, 10, 70, &a, 2}, {3, 20, 80, &b, 1}}, 4, 99, 3, 64, 2);
68+
ASSERT_TRUE(plan);
69+
const auto ids = plan->ids;
70+
ASSERT_TRUE(df::pad_to_static(*plan, 2, 4, 2));
71+
EXPECT_EQ(plan->ids, ids);
72+
EXPECT_EQ(
73+
plan->hidden,
74+
std::vector<std::uint8_t>(
75+
{1, 1, 2, 2, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}));
76+
// Context rows 0..2, padding 3..7, queries 8..15.
77+
EXPECT_EQ(
78+
plan->positions,
79+
std::vector<std::int64_t>(
80+
{10, 11, 20, 0, 0, 0, 0, 0, 12, 13, 14, 15, 21, 22, 23, 24}));
81+
EXPECT_EQ(
82+
plan->kv_order,
83+
std::vector<std::int64_t>(
84+
{0, 1, 8, 9, 10, 11, 2, 12, 13, 14, 15, 3, 4, 5, 6, 7}));
85+
}
86+
87+
TEST(CudaDFlash, StaticPaddingRejectsLongContext) {
88+
const std::vector<std::uint8_t> a(2 * 5, 1);
89+
auto plan = df::pack_draft({{9, 10, 70, &a, 5}}, 4, 99, 5, 64, 2);
90+
ASSERT_TRUE(plan);
91+
EXPECT_FALSE(df::pad_to_static(*plan, 1, 4, 2));
92+
}
93+
6494
TEST(CudaDFlash, NewPreambleAnchorIsRetainedOnce) {
6595
// The input token (7) has already committed. The regular preamble predicts
6696
// 10, then verification consumes 10 and predicts the accepted/corrected tail.

0 commit comments

Comments
 (0)