2323#include < executorch/extension/llm/sampler/util.h>
2424#include < executorch/extension/module/module.h>
2525#include < executorch/extension/tensor/tensor.h>
26+ #include < executorch/extension/tensor/tensor_ptr.h>
2627#include < executorch/runtime/backend/interface.h>
2728#include < executorch/runtime/backend/options.h>
29+ #include < executorch/runtime/core/portable_type/device.h>
30+ #include < executorch/runtime/platform/assert.h>
2831#include < executorch/runtime/platform/log.h>
2932#include < pytorch/tokenizers/hf_tokenizer.h>
3033
@@ -79,24 +82,34 @@ DEFINE_bool(
7982
8083namespace llm = ::executorch::extension::llm;
8184using ::executorch::extension::from_blob;
85+ using ::executorch::extension::make_tensor_ptr;
8286using ::executorch::extension::Module;
87+ using ::executorch::extension::TensorPtr;
8388using ::executorch::runtime::Error;
8489using ::executorch::runtime::EValue;
90+ #ifdef EXECUTORCH_BUILD_CUDA
91+ using ::executorch::extension::clone_tensor_ptr_to;
92+ #endif
8593
8694using SizesType = executorch::aten::SizesType;
8795
88- // Read a sampled token ID from a scalar float output (CUDA path).
96+ // Read a sampled token ID from a scalar int64 output (CUDA path).
97+ //
98+ // The model now emits the sampled token as int64 (see sampler.py), matching
99+ // the decode method's int64 token input so the on-device output buffer can be
100+ // aliased directly as the next step's input. We still copy the 8-byte scalar
101+ // back to the host here for EOS detection and detokenization.
89102static uint64_t read_token (const executorch::aten::Tensor& output) {
90103 const void * ptr = output.const_data_ptr ();
91- float val = 0 . 0f ;
104+ int64_t val = 0 ;
92105
93106#ifdef EXECUTORCH_BUILD_CUDA
94107 cudaPointerAttributes attrs{};
95108 bool on_device = cudaPointerGetAttributes (&attrs, ptr) == cudaSuccess &&
96109 attrs.type == cudaMemoryTypeDevice;
97110 if (on_device) {
98111 cudaError_t err =
99- cudaMemcpy (&val, ptr, sizeof (float ), cudaMemcpyDeviceToHost);
112+ cudaMemcpy (&val, ptr, sizeof (int64_t ), cudaMemcpyDeviceToHost);
100113 if (err != cudaSuccess) {
101114 ET_LOG (
102115 Error,
@@ -105,13 +118,13 @@ static uint64_t read_token(const executorch::aten::Tensor& output) {
105118 return 0 ;
106119 }
107120 } else {
108- memcpy (&val, ptr, sizeof (float ));
121+ memcpy (&val, ptr, sizeof (int64_t ));
109122 }
110123#else
111- memcpy (&val, ptr, sizeof (float ));
124+ memcpy (&val, ptr, sizeof (int64_t ));
112125#endif
113126
114- return static_cast <uint64_t >(llrintf ( val) );
127+ return static_cast <uint64_t >(val);
115128}
116129
117130int main (int argc, char ** argv) {
@@ -181,6 +194,8 @@ int main(int argc, char** argv) {
181194 FLAGS_temperature <= 0.0 ? 1e-6f : static_cast <float >(FLAGS_temperature);
182195
183196#ifdef EXECUTORCH_BUILD_CUDA
197+ const auto cuda_device =
198+ executorch::aten::Device (executorch::aten::DeviceType::CUDA , 0 );
184199 if (FLAGS_cuda_graph) {
185200 executorch::runtime::BackendOptions<2 > cuda_opts;
186201 cuda_opts.set_option (" enable_cuda_graph_for_method" , " decode" );
@@ -217,8 +232,9 @@ int main(int argc, char** argv) {
217232 ET_LOG (Error, " Failed to load decode method" );
218233 return 1 ;
219234 }
220- auto temp_tensor =
221- from_blob (&temp_val, {1 }, executorch::aten::ScalarType::Float);
235+ auto temp_tensor = clone_tensor_ptr_to (
236+ from_blob (&temp_val, {1 }, executorch::aten::ScalarType::Float),
237+ cuda_device);
222238#else
223239 if (FLAGS_cuda_graph) {
224240 ET_LOG (Info, " --cuda_graph ignored on non-CUDA build" );
@@ -286,6 +302,12 @@ int main(int argc, char** argv) {
286302 // ---------------------------------------------------------------
287303 uint64_t cur_token = 0 ;
288304 int64_t prefill_pos = 0 ;
305+ #ifdef EXECUTORCH_BUILD_CUDA
306+ // Alias of the most recent forward's on-device int64 output token. The last
307+ // prefill chunk's output seeds the first decode step (no token H2D); each
308+ // decode step then re-aliases its own output for the next step.
309+ TensorPtr device_out_token;
310+ #endif
289311 while (prefill_pos < num_prompt_tokens) {
290312 int64_t chunk_len =
291313 std::min (num_prompt_tokens - prefill_pos, max_prefill_chunk);
@@ -304,6 +326,12 @@ int main(int argc, char** argv) {
304326 auto pos_tensor = from_blob (
305327 pos_data.data (), {S (chunk_len)}, executorch::aten::ScalarType::Long);
306328
329+ #ifdef EXECUTORCH_BUILD_CUDA
330+ // skip_h2d: prefill/decode method inputs must already live in CUDA memory.
331+ tokens_tensor = clone_tensor_ptr_to (tokens_tensor, cuda_device);
332+ pos_tensor = clone_tensor_ptr_to (pos_tensor, cuda_device);
333+ #endif
334+
307335 std::vector<EValue> inputs;
308336 inputs.push_back (EValue (tokens_tensor));
309337 inputs.push_back (EValue (pos_tensor));
@@ -322,7 +350,11 @@ int main(int argc, char** argv) {
322350 }
323351
324352#ifdef EXECUTORCH_BUILD_CUDA
325- cur_token = read_token (result.get ()[0 ].toTensor ());
353+ const auto & out_tensor = result.get ()[0 ].toTensor ();
354+ cur_token = read_token (out_tensor);
355+ // Keep the sampled token on device: alias the output buffer so it feeds
356+ // straight into the next forward as the int64 token input (zero copy).
357+ device_out_token = make_tensor_ptr (out_tensor);
326358#else
327359 cur_token = static_cast <uint64_t >(
328360 llm::logits_to_token (result.get ()[0 ].toTensor (), temp_val));
@@ -354,21 +386,69 @@ int main(int argc, char** argv) {
354386 // Decode loop
355387 // ---------------------------------------------------------------
356388 int64_t pos = num_prompt_tokens;
357- std::vector<int64_t > decode_token_data = {static_cast <int64_t >(cur_token)};
358389 std::vector<int64_t > decode_pos_data = {pos};
390+ auto decode_pos_cpu = from_blob (
391+ decode_pos_data.data (), {1 }, executorch::aten::ScalarType::Long);
392+ #ifdef EXECUTORCH_BUILD_CUDA
393+ // Fixed device-resident position input slot: the decode method always reads
394+ // the position from this same address every step (cuda-graph-safe). Seeded
395+ // once here with a one-time H2D; refreshed each step by an on-device D2D.
396+ auto decode_pos = clone_tensor_ptr_to (decode_pos_cpu, cuda_device);
397+ // Upload the FULL decode position array to device ONCE (a single H2D - the
398+ // one-time copy we keep). Each step copies its position from here into the
399+ // fixed slot with a device-to-device copy, so there is NO per-round pos H2D.
400+ std::vector<int64_t > pos_seq_data (FLAGS_max_new_tokens);
401+ for (int32_t i = 0 ; i < FLAGS_max_new_tokens; i++) {
402+ pos_seq_data[i] = num_prompt_tokens + i;
403+ }
404+ auto pos_seq_dev = clone_tensor_ptr_to (
405+ from_blob (
406+ pos_seq_data.data (),
407+ {S (FLAGS_max_new_tokens)},
408+ executorch::aten::ScalarType::Long),
409+ cuda_device);
410+ auto * pos_seq_dev_ptr =
411+ static_cast <int64_t *>(pos_seq_dev->mutable_data_ptr ());
412+ auto * decode_pos_slot_ptr =
413+ static_cast <int64_t *>(decode_pos->mutable_data_ptr ());
414+ #else
415+ // Non-CUDA (MLX) path: keep host token/pos buffers; the backend stages them
416+ // and the host samples from the returned logits.
417+ std::vector<int64_t > decode_token_data = {static_cast <int64_t >(cur_token)};
359418 auto decode_tokens = from_blob (
360419 decode_token_data.data (), {1 , 1 }, executorch::aten::ScalarType::Long);
361- auto decode_pos = from_blob (
362- decode_pos_data. data (), { 1 }, executorch::aten::ScalarType::Long);
420+ auto decode_pos = decode_pos_cpu;
421+ # endif
363422
364423 uint64_t prev_token = cur_token;
365424 bool hit_eos = eos_ids.find (cur_token) != eos_ids.end ();
366425 for (int32_t step = 0 ; step < FLAGS_max_new_tokens && !hit_eos; step++) {
367- decode_token_data[0 ] = static_cast <int64_t >(cur_token);
426+ #ifdef EXECUTORCH_BUILD_CUDA
427+ // No per-round H2D: copy this step's position from the pre-uploaded device
428+ // position array into the fixed position slot with an on-device D2D. With
429+ // the token aliased on device (Option A) and the position staged via D2D,
430+ // the per-round HtoD count is zero (independent of decode length).
431+ // cudaMemcpy D2D is host-synchronous, so the slot is updated before the
432+ // decode kernels read it; with cuda graph enabled this becomes a captured
433+ // cudaMemcpyAsync on the decode stream into this same fixed slot.
434+ ET_CHECK_MSG (
435+ cudaMemcpy (
436+ decode_pos_slot_ptr,
437+ pos_seq_dev_ptr + step,
438+ sizeof (int64_t ),
439+ cudaMemcpyDeviceToDevice) == cudaSuccess,
440+ " Failed to copy decode position D2D" );
441+ #else
368442 decode_pos_data[0 ] = pos;
443+ decode_token_data[0 ] = static_cast <int64_t >(cur_token);
444+ #endif
369445
370446 std::vector<EValue> inputs;
447+ #ifdef EXECUTORCH_BUILD_CUDA
448+ inputs.push_back (EValue (device_out_token));
449+ #else
371450 inputs.push_back (EValue (decode_tokens));
451+ #endif
372452 inputs.push_back (EValue (decode_pos));
373453
374454#ifdef EXECUTORCH_BUILD_CUDA
@@ -385,7 +465,10 @@ int main(int argc, char** argv) {
385465
386466 prev_token = cur_token;
387467#ifdef EXECUTORCH_BUILD_CUDA
388- cur_token = read_token (result.get ()[0 ].toTensor ());
468+ const auto & out_tensor = result.get ()[0 ].toTensor ();
469+ cur_token = read_token (out_tensor);
470+ // Alias this step's on-device output token as the next step's token input.
471+ device_out_token = make_tensor_ptr (out_tensor);
389472#else
390473 cur_token = static_cast <uint64_t >(
391474 llm::logits_to_token (result.get ()[0 ].toTensor (), temp_val));
0 commit comments