Skip to content

Commit 69c7ce2

Browse files
[executorch][cuda] Run CudaExecutor steps from prepared embeddings (#24653)
This PR was created by the merge bot to help merge the original PR into the main branch. ghstack PR number: #24063 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/86/base ghstack PR head: https://github.com/pytorch/executorch/tree/gh/Gasoonjia/86/head Merge bot PR base: https://github.com/pytorch/executorch/tree/gh/Gasoonjia/85/orig Merge bot PR head: https://github.com/pytorch/executorch/tree/gh/Gasoonjia/86/orig Differential Revision: [D125987291](https://our.internmc.facebook.com/intern/diff/D125987291/) @diff-train-skip-merge Co-authored-by: gasoonjia <gasoonjia@icloud.com>
1 parent e25bd5a commit 69c7ce2

5 files changed

Lines changed: 309 additions & 6 deletions

File tree

‎backends/cuda/batching/cuda_executor.cpp‎

Lines changed: 203 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,37 @@ Result<int> step_width(const MethodMeta& meta, const char* method) {
9090
return static_cast<int>(token_sizes[1]);
9191
}
9292

93+
// kEmbeddingsMethod's decoder input: [1, T, hidden] in a float dtype, with
94+
// Long[T] positions and a rank-one Long selector. Returns T's upper bound.
95+
Result<std::pair<int, std::int64_t>> embeddings_width(
96+
const MethodMeta& meta,
97+
::executorch::aten::ScalarType* dtype) {
98+
const auto embeddings = meta.input_tensor_meta(0);
99+
const auto positions = meta.input_tensor_meta(1);
100+
const auto selector = meta.input_tensor_meta(2);
101+
ET_CHECK_OR_RETURN_ERROR(
102+
meta.num_inputs() == 3 && embeddings.ok() && positions.ok() &&
103+
selector.ok(),
104+
InvalidProgram,
105+
"CudaExecutor: %s must take embeddings, positions and a logits selector",
106+
kEmbeddingsMethod);
107+
const auto sizes = embeddings->sizes();
108+
const auto position_sizes = positions->sizes();
109+
ET_CHECK_OR_RETURN_ERROR(
110+
is_supported_logits_type(embeddings->scalar_type()) &&
111+
sizes.size() == 3 && sizes[0] == 1 && sizes[1] > 0 && sizes[2] > 0 &&
112+
positions->scalar_type() == ::executorch::aten::ScalarType::Long &&
113+
position_sizes.size() == 1 && position_sizes[0] == sizes[1] &&
114+
selector->scalar_type() == ::executorch::aten::ScalarType::Long &&
115+
selector->sizes().size() == 1,
116+
InvalidProgram,
117+
"CudaExecutor: %s must take [1, T, hidden] embeddings, Long[T] positions "
118+
"and a rank-one Long selector",
119+
kEmbeddingsMethod);
120+
*dtype = embeddings->scalar_type();
121+
return std::pair<int, std::int64_t>{static_cast<int>(sizes[1]), sizes[2]};
122+
}
123+
93124
// The logits width `method` produces.
94125
Result<std::int64_t> logits_width(const MethodMeta& meta, const char* method) {
95126
ET_CHECK_OR_RETURN_ERROR(
@@ -490,8 +521,27 @@ Result<std::unique_ptr<CudaExecutor>> CudaExecutor::create(
490521
max_sessions,
491522
seq_limit.value_or(0));
492523

524+
std::optional<EmbeddingsMethod> embeddings_method;
525+
if (names.count(kEmbeddingsMethod) != 0) {
526+
ET_ASSIGN_OR_RETURN(meta, module->method_meta(kEmbeddingsMethod));
527+
::executorch::aten::ScalarType dtype{};
528+
ET_ASSIGN_OR_RETURN(width, embeddings_width(meta, &dtype));
529+
ET_ASSIGN_OR_RETURN(vocab, logits_width(meta, kEmbeddingsMethod));
530+
ET_CHECK_OK_OR_RETURN_ERROR(backend_of(meta, kEmbeddingsMethod).error());
531+
ET_CHECK_OR_RETURN_ERROR(
532+
vocab == method_vocab && width.first <= max_step_tokens &&
533+
min_selected_rows <= width.first,
534+
InvalidProgram,
535+
"CudaExecutor: %s disagrees with the token forwards",
536+
kEmbeddingsMethod);
537+
// Exported, like forward_others, from min_selected_rows tokens; narrower
538+
// steps pad up to it.
539+
embeddings_method =
540+
EmbeddingsMethod{width.first, min_selected_rows, width.second, dtype};
541+
}
542+
493543
ET_CHECK_OK_OR_RETURN_ERROR(set_backend_options(options, methods));
494-
return std::unique_ptr<CudaExecutor>(new CudaExecutor(
544+
auto executor = std::unique_ptr<CudaExecutor>(new CudaExecutor(
495545
std::move(module),
496546
std::move(cache),
497547
max_sessions,
@@ -502,6 +552,8 @@ Result<std::unique_ptr<CudaExecutor>> CudaExecutor::create(
502552
min_selected_rows,
503553
has_sampler,
504554
has_argmax));
555+
executor->embeddings_method_ = embeddings_method;
556+
return executor;
505557
}
506558

507559
bool CudaExecutor::initialize() {
@@ -524,6 +576,15 @@ bool CudaExecutor::initialize() {
524576
return false;
525577
}
526578
}
579+
if (embeddings_method_ &&
580+
module_->load_method(
581+
kEmbeddingsMethod,
582+
/*planned_memory=*/nullptr,
583+
/*event_tracer=*/nullptr,
584+
&options_map) != Error::Ok) {
585+
ET_LOG(Error, "CudaExecutor: could not load %s", kEmbeddingsMethod);
586+
return false;
587+
}
527588
for (const auto& [present, name] :
528589
{std::pair{has_sampler_, kSampleMethod},
529590
std::pair{has_argmax_, kArgmaxMethod}}) {
@@ -532,6 +593,11 @@ bool CudaExecutor::initialize() {
532593
return false;
533594
}
534595
}
596+
if (embedder_ && embedder_->initialize &&
597+
embedder_->initialize() != Error::Ok) {
598+
ET_LOG(Error, "CudaExecutor: the input embedder failed to initialize");
599+
return false;
600+
}
535601
initialized_ = true;
536602
return true;
537603
}
@@ -555,6 +621,25 @@ void CudaExecutor::set_sampling(
555621
sessions_.set_sampling(session, params, seed);
556622
}
557623

624+
bool CudaExecutor::accepts(const llm_batching::PreparedInput& input) const {
625+
return embedder_ && embedder_->accepts && embedder_->accepts(input);
626+
}
627+
628+
Error CudaExecutor::set_input_embedder(InputEmbedder embedder) {
629+
ET_CHECK_OR_RETURN_ERROR(
630+
!initialized_ && embedder.accepts && embedder.materialize,
631+
InvalidArgument,
632+
"CudaExecutor: an embedder needs accepts and materialize, before "
633+
"initialize()");
634+
ET_CHECK_OR_RETURN_ERROR(
635+
embeddings_method_.has_value(),
636+
NotSupported,
637+
"CudaExecutor: the program exports no %s",
638+
kEmbeddingsMethod);
639+
embedder_ = std::move(embedder);
640+
return Error::Ok;
641+
}
642+
558643
bool CudaExecutor::execute_raw(
559644
const RawTargetStep& step,
560645
const std::function<bool(const RawTargetOutput&)>& consume) {
@@ -590,6 +675,9 @@ bool CudaExecutor::execute_raw(
590675
}
591676
bool& flag;
592677
} reset(raw_executing_);
678+
if (step.embeddings) {
679+
return execute_embeddings(step, consume);
680+
}
593681
for (const auto& slice : plan_slices(total, methods_)) {
594682
const int off = slice.offset;
595683
const int n = slice.length;
@@ -756,6 +844,18 @@ bool CudaExecutor::execute(const BatchInput& batch, BatchOutput& out) {
756844
}
757845

758846
RawTargetStep raw;
847+
if (step->prepared) {
848+
if (!embedder_) {
849+
ET_LOG(Error, "CudaExecutor: a prepared input needs an input embedder");
850+
return false;
851+
}
852+
auto embeddings = embedder_->materialize(batch.inputs);
853+
if (!embeddings.ok()) {
854+
ET_LOG(Error, "CudaExecutor: could not materialize the batch's inputs");
855+
return false;
856+
}
857+
raw.embeddings = std::move(*embeddings);
858+
}
759859
raw.tokens = std::move(step->tokens);
760860
raw.positions = std::move(step->positions);
761861
raw.seq_ids = std::move(step->seq_ids);
@@ -954,11 +1054,112 @@ OffGraphKVMetrics CudaExecutor::kv_metrics() const {
9541054

9551055
std::vector<ForwardMethodCalls> CudaExecutor::method_calls() const {
9561056
std::vector<ForwardMethodCalls> calls;
957-
calls.reserve(methods_.size());
1057+
calls.reserve(methods_.size() + 1);
9581058
for (std::size_t i = 0; i < methods_.size(); ++i) {
9591059
calls.push_back({methods_[i].name, calls_[i]});
9601060
}
1061+
if (embeddings_method_) {
1062+
calls.push_back({kEmbeddingsMethod, embeddings_method_->calls});
1063+
}
9611064
return calls;
9621065
}
9631066

1067+
bool CudaExecutor::execute_embeddings(
1068+
const RawTargetStep& step,
1069+
const std::function<bool(const RawTargetOutput&)>& consume) {
1070+
using ::executorch::aten::SizesType;
1071+
const auto& embeddings = *step.embeddings;
1072+
const int total = static_cast<int>(step.positions.size());
1073+
if (!embeddings_method_ || embeddings.dim() != 3 || embeddings.size(0) != 1 ||
1074+
embeddings.size(1) != total ||
1075+
embeddings.size(2) != embeddings_method_->hidden ||
1076+
embeddings.scalar_type() != embeddings_method_->dtype ||
1077+
!embeddings.device().is_cpu() || !is_dense(embeddings) || total <= 0 ||
1078+
embeddings.const_data_ptr() == nullptr) {
1079+
ET_LOG(
1080+
Error,
1081+
"CudaExecutor: embeddings must be host [1, %d, %" PRId64 "] tensors",
1082+
total,
1083+
embeddings_method_ ? embeddings_method_->hidden : 0);
1084+
return false;
1085+
}
1086+
const auto& method = *embeddings_method_;
1087+
const std::size_t row_bytes = static_cast<std::size_t>(method.hidden) *
1088+
::executorch::runtime::elementSize(method.dtype);
1089+
const auto* data =
1090+
static_cast<const std::uint8_t*>(embeddings.const_data_ptr());
1091+
for (int off = 0; off < total; off += method.max_tokens) {
1092+
const int n = std::min(method.max_tokens, total - off);
1093+
const int width = std::max(n, method.min_tokens);
1094+
std::vector<std::int64_t> selected;
1095+
std::vector<std::size_t> indices;
1096+
for (std::size_t i = 0; i < step.logit_indices.size(); ++i) {
1097+
const auto index = step.logit_indices[i];
1098+
if (index >= off && index < off + n) {
1099+
selected.push_back(index - off);
1100+
indices.push_back(i);
1101+
}
1102+
}
1103+
if (selected.size() > static_cast<std::size_t>(method.max_tokens)) {
1104+
return false;
1105+
}
1106+
const std::size_t rows =
1107+
std::max<std::size_t>(selected.size(), min_selected_rows_);
1108+
selected.resize(rows, selected.empty() ? 0 : selected.back());
1109+
if (!ctl_->declare_step(std::vector<std::int32_t>(
1110+
step.seq_ids.begin() + off, step.seq_ids.begin() + off + n))) {
1111+
return false;
1112+
}
1113+
// Rows past n are padding the cache writes to its scratch cell.
1114+
std::vector<std::uint8_t> rows_bytes(
1115+
static_cast<std::size_t>(width) * row_bytes, 0);
1116+
std::copy(
1117+
data + static_cast<std::size_t>(off) * row_bytes,
1118+
data + static_cast<std::size_t>(off + n) * row_bytes,
1119+
rows_bytes.begin());
1120+
auto input = make_tensor_ptr(
1121+
{1,
1122+
static_cast<SizesType>(width),
1123+
static_cast<SizesType>(method.hidden)},
1124+
std::move(rows_bytes),
1125+
method.dtype);
1126+
std::vector<std::int64_t> position_values(
1127+
step.positions.begin() + off, step.positions.begin() + off + n);
1128+
position_values.resize(width, 0);
1129+
auto positions = make_tensor_ptr(
1130+
{static_cast<SizesType>(width)}, std::move(position_values));
1131+
auto selector =
1132+
make_tensor_ptr({static_cast<SizesType>(rows)}, std::move(selected));
1133+
auto result =
1134+
module_->execute(kEmbeddingsMethod, {input, positions, selector});
1135+
if (!result.ok() || result->empty() || !result->at(0).isTensor()) {
1136+
ET_LOG(Error, "CudaExecutor: %s failed", kEmbeddingsMethod);
1137+
return false;
1138+
}
1139+
++embeddings_method_->calls;
1140+
const auto logits = result->at(0).toTensor();
1141+
if (logits.dim() < 2 || logits.size(logits.dim() - 1) != vocab_size_ ||
1142+
logits.numel() != static_cast<std::int64_t>(rows) * vocab_size_ ||
1143+
!is_supported_logits_type(logits.scalar_type()) || !is_dense(logits)) {
1144+
return false;
1145+
}
1146+
std::optional<::executorch::aten::Tensor> taps;
1147+
if (result->size() > 1) {
1148+
if (!result->at(1).isTensor()) {
1149+
return false;
1150+
}
1151+
taps = result->at(1).toTensor();
1152+
if (taps->dim() != 3 || taps->size(0) != 1 || taps->size(1) != width ||
1153+
taps->size(2) <= 0 ||
1154+
!is_supported_logits_type(taps->scalar_type()) || !is_dense(*taps)) {
1155+
return false;
1156+
}
1157+
}
1158+
if (!consume({off, n, logits, taps ? &*taps : nullptr, indices})) {
1159+
return false;
1160+
}
1161+
}
1162+
return true;
1163+
}
1164+
9641165
} // namespace executorch::backends::cuda::batching

‎backends/cuda/batching/cuda_executor.h‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
#include <executorch/extension/llm/cache/cache.h>
3434
#include <executorch/extension/llm/cache/cache_registry.h>
3535
#include <executorch/extension/module/module.h>
36+
#include <executorch/extension/tensor/tensor_ptr.h>
3637
#include <executorch/runtime/core/result.h>
3738
#include <executorch/runtime/platform/compiler.h> // ET_EXPERIMENTAL
3839

@@ -56,6 +57,10 @@ inline constexpr char kMinSelectedRowsMethod[] = "get_min_prefill_chunk";
5657
// both returning Long [R], and only the tokens come back to the host.
5758
inline constexpr char kSampleMethod[] = "sample";
5859
inline constexpr char kArgmaxMethod[] = "sample_argmax";
60+
// Optional dynamic forward over the same cache that takes decoder inputs
61+
// rather than tokens: (embeddings [1, T, hidden], Long[T] positions, Long[R]
62+
// selector). It runs only the steps carrying prepared inputs, e.g. images.
63+
inline constexpr char kEmbeddingsMethod[] = "forward_from_embeddings";
5964

6065
// Process-wide CUDA backend options create() sets before the methods load.
6166
struct CudaExecutorOptions {
@@ -97,6 +102,22 @@ struct RawTargetStep {
97102
// Global token-row indices; -1 discards a result. Repeats and reordering are
98103
// allowed. Callback selected_indices maps rows back into this vector.
99104
std::vector<std::int64_t> logit_indices;
105+
// Host [1, tokens, hidden] decoder inputs. When set, the step runs through
106+
// kEmbeddingsMethod and `tokens` is not read.
107+
::executorch::extension::TensorPtr embeddings;
108+
};
109+
110+
// Turns a batch holding prepared inputs into its decoder inputs, for a model
111+
// whose prompts carry more than tokens. Runs on the engine thread.
112+
struct InputEmbedder {
113+
// Loads what materialize needs; called from initialize().
114+
std::function<::executorch::runtime::Error()> initialize;
115+
std::function<bool(const llm_batching::PreparedInput&)> accepts;
116+
// Host [1, rows, hidden] embeddings for every row of `inputs`, in order.
117+
std::function<
118+
::executorch::runtime::Result<::executorch::extension::TensorPtr>(
119+
const std::vector<llm_batching::Input>&)>
120+
materialize;
100121
};
101122

102123
struct RawTargetOutput {
@@ -162,6 +183,16 @@ class ET_EXPERIMENTAL CudaExecutor : public llm_batching::Executor {
162183
bool execute(
163184
const llm_batching::BatchInput& batch,
164185
llm_batching::BatchOutput& out) override;
186+
// Whether the installed embedder takes `input`; false without one.
187+
bool accepts(const llm_batching::PreparedInput& input) const override;
188+
189+
// Installs the embedder for prepared inputs, before initialize(). The program
190+
// must export kEmbeddingsMethod.
191+
::executorch::runtime::Error set_input_embedder(InputEmbedder embedder);
192+
// The decoder input width kEmbeddingsMethod takes, or 0 without it.
193+
std::int64_t embeddings_hidden_size() const {
194+
return embeddings_method_ ? embeddings_method_->hidden : 0;
195+
}
165196

166197
// Engine-thread-only raw access for model adapters. Output storage is
167198
// borrowed until the callback returns: copy/consume every needed output
@@ -230,6 +261,9 @@ class ET_EXPERIMENTAL CudaExecutor : public llm_batching::Executor {
230261
std::optional<std::vector<llm_batching::Token>> sample_on_device(
231262
const ::executorch::aten::Tensor& logits,
232263
const std::vector<llm_batching::SessionId>& sessions);
264+
bool execute_embeddings(
265+
const RawTargetStep& step,
266+
const std::function<bool(const RawTargetOutput&)>& consume);
233267

234268
// Ordered so the module dies first, releasing the delegates that resolved
235269
// the cache before the registry entry naming it goes.
@@ -243,6 +277,15 @@ class ET_EXPERIMENTAL CudaExecutor : public llm_batching::Executor {
243277
// Sorted by max_tokens, ascending; the dynamic method, if any, is last.
244278
std::vector<ForwardMethod> methods_;
245279
std::vector<std::uint64_t> calls_;
280+
struct EmbeddingsMethod {
281+
int max_tokens;
282+
int min_tokens;
283+
std::int64_t hidden;
284+
::executorch::aten::ScalarType dtype;
285+
std::uint64_t calls = 0;
286+
};
287+
std::optional<EmbeddingsMethod> embeddings_method_;
288+
std::optional<InputEmbedder> embedder_;
246289
int min_selected_rows_;
247290
llm_batching::util::SessionTable sessions_;
248291
bool has_sampler_;

0 commit comments

Comments
 (0)