Skip to content

Commit 374f7b5

Browse files
authored
skip h2d copies between forward functions in gemma4-31b (#20286)
Summary: This diff updates gemma4-31b export and runtime pipeline to skip the h2d and d2h copies between prefill and decode, and between previous round next round of decode as well.   | MAIN | Current -- | -- | -- total H2D (d=128)| 388 / 417 µs | 5 / 7.4 µs per-round H2D | 3 | 0 decode tok/s | 45.86 | 47.02 Differential Revision: D108661628
1 parent 3669695 commit 374f7b5

4 files changed

Lines changed: 112 additions & 17 deletions

File tree

examples/models/gemma4_31b/export.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ def _export_cuda(
171171
)
172172
from executorch.exir.backend.compile_spec_schema import CompileSpec
173173
from executorch.exir.passes import MemoryPlanningPass
174+
from executorch.exir.passes.propagate_device_pass import PropagateDeviceConfig
174175
from torch.export import Dim, export
175176

176177
inductor_config.coordinate_descent_tuning = False
@@ -270,6 +271,14 @@ def _export_cuda(
270271
alloc_graph_input=False,
271272
),
272273
emit_mutable_buffer_names=True,
274+
# Keep method inputs/outputs device-resident so the CUDA backend
275+
# does not insert boundary H2D/D2H copies: the runner stages inputs
276+
# in CUDA memory and reads the sampled token back with a single
277+
# small D2H. CUDA-only (no effect on the MLX path).
278+
propagate_device_config=PropagateDeviceConfig(
279+
skip_h2d_for_method_inputs=True,
280+
skip_d2h_for_method_outputs=True,
281+
),
273282
),
274283
)
275284

examples/models/gemma4_31b/main.cpp

Lines changed: 97 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,11 @@
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

8083
namespace llm = ::executorch::extension::llm;
8184
using ::executorch::extension::from_blob;
85+
using ::executorch::extension::make_tensor_ptr;
8286
using ::executorch::extension::Module;
87+
using ::executorch::extension::TensorPtr;
8388
using ::executorch::runtime::Error;
8489
using ::executorch::runtime::EValue;
90+
#ifdef EXECUTORCH_BUILD_CUDA
91+
using ::executorch::extension::clone_tensor_ptr_to;
92+
#endif
8593

8694
using 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.
89102
static 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

117130
int 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));

examples/models/gemma4_31b/model.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,7 @@ def forward(
484484
temperature: 1-D float tensor for Gumbel-max sampling.
485485
486486
Returns:
487-
(B, 1) sampled token IDs as float.
487+
(B, 1) sampled token IDs as int64.
488488
"""
489489
x = self.embed_tokens(tokens) * self.embed_normalizer
490490

examples/models/gemma4_31b/sampler.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,12 @@ def sample(
2626
temperature still works ("near-greedy").
2727
2828
Returns:
29-
``[B, 1]`` float32 token IDs (``argmax(logits/T + gumbel_noise)``).
29+
``[B, 1]`` int64 token IDs (``argmax(logits/T + gumbel_noise)``).
30+
Emitting int64 (rather than casting to float) lets the runner alias the
31+
on-device output token directly as the next decode step's int64 token
32+
input — no D2H/H2D round-trip and no dtype cast.
3033
"""
3134
logits = logits / temperature.clamp(min=1e-6)
3235
noise = torch.rand_like(logits)
3336
gumbel = -torch.log(-torch.log(noise + 1e-20) + 1e-20)
34-
return (logits + gumbel).argmax(dim=-1, keepdim=True).float()
37+
return (logits + gumbel).argmax(dim=-1, keepdim=True).to(torch.int64)

0 commit comments

Comments
 (0)