Skip to content

Commit b486c56

Browse files
committed
gemma4_31b: keep sampled token on device (int64 output + zero-copy alias)
Eliminate the per-decode-round token D2H->H2D round-trip: - sampler.py: emit the sampled token as int64 (was float32) so the decode method's int64 token output can be aliased directly as the next forward's int64 token input (value-preserving: argmax index; token ids < 2^24). - main.cpp: read_token reads int64; each forward's on-device output token is aliased via make_tensor_ptr and fed straight back as the next step's token input (prefill->decode and decode->decode). Only the per-round position H2D remains. Measured (int6/gguf, cuda graph OFF, p19/d128): post-load HtoD 261->132 (token H2D removed; ~= decode length); DtoH/DtoD counts unchanged (129), bytes 4B->8B (token now int64). Greedy output byte-identical to prior export.
1 parent f73970e commit b486c56

3 files changed

Lines changed: 50 additions & 32 deletions

File tree

examples/models/gemma4_31b/main.cpp

Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,9 @@ DEFINE_bool(
8282

8383
namespace llm = ::executorch::extension::llm;
8484
using ::executorch::extension::from_blob;
85+
using ::executorch::extension::make_tensor_ptr;
8586
using ::executorch::extension::Module;
87+
using ::executorch::extension::TensorPtr;
8688
using ::executorch::runtime::Error;
8789
using ::executorch::runtime::EValue;
8890
#ifdef EXECUTORCH_BUILD_CUDA
@@ -91,18 +93,23 @@ using ::executorch::extension::clone_tensor_ptr_to;
9193

9294
using SizesType = executorch::aten::SizesType;
9395

94-
// 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.
95102
static uint64_t read_token(const executorch::aten::Tensor& output) {
96103
const void* ptr = output.const_data_ptr();
97-
float val = 0.0f;
104+
int64_t val = 0;
98105

99106
#ifdef EXECUTORCH_BUILD_CUDA
100107
cudaPointerAttributes attrs{};
101108
bool on_device = cudaPointerGetAttributes(&attrs, ptr) == cudaSuccess &&
102109
attrs.type == cudaMemoryTypeDevice;
103110
if (on_device) {
104111
cudaError_t err =
105-
cudaMemcpy(&val, ptr, sizeof(float), cudaMemcpyDeviceToHost);
112+
cudaMemcpy(&val, ptr, sizeof(int64_t), cudaMemcpyDeviceToHost);
106113
if (err != cudaSuccess) {
107114
ET_LOG(
108115
Error,
@@ -111,13 +118,13 @@ static uint64_t read_token(const executorch::aten::Tensor& output) {
111118
return 0;
112119
}
113120
} else {
114-
memcpy(&val, ptr, sizeof(float));
121+
memcpy(&val, ptr, sizeof(int64_t));
115122
}
116123
#else
117-
memcpy(&val, ptr, sizeof(float));
124+
memcpy(&val, ptr, sizeof(int64_t));
118125
#endif
119126

120-
return static_cast<uint64_t>(llrintf(val));
127+
return static_cast<uint64_t>(val);
121128
}
122129

123130
int main(int argc, char** argv) {
@@ -295,6 +302,12 @@ int main(int argc, char** argv) {
295302
// ---------------------------------------------------------------
296303
uint64_t cur_token = 0;
297304
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
298311
while (prefill_pos < num_prompt_tokens) {
299312
int64_t chunk_len =
300313
std::min(num_prompt_tokens - prefill_pos, max_prefill_chunk);
@@ -337,7 +350,11 @@ int main(int argc, char** argv) {
337350
}
338351

339352
#ifdef EXECUTORCH_BUILD_CUDA
340-
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);
341358
#else
342359
cur_token = static_cast<uint64_t>(
343360
llm::logits_to_token(result.get()[0].toTensor(), temp_val));
@@ -369,53 +386,48 @@ int main(int argc, char** argv) {
369386
// Decode loop
370387
// ---------------------------------------------------------------
371388
int64_t pos = num_prompt_tokens;
372-
std::vector<int64_t> decode_token_data = {static_cast<int64_t>(cur_token)};
373389
std::vector<int64_t> decode_pos_data = {pos};
374-
auto decode_tokens_cpu = from_blob(
375-
decode_token_data.data(), {1, 1}, executorch::aten::ScalarType::Long);
376390
auto decode_pos_cpu = from_blob(
377391
decode_pos_data.data(), {1}, executorch::aten::ScalarType::Long);
378392
#ifdef EXECUTORCH_BUILD_CUDA
379-
// skip_h2d: keep fixed device-resident input buffers across decode steps
380-
// (seeded here with a one-time H2D). Their 8-byte contents are refreshed
381-
// each step from the host (see loop), since the sampled id round-trips to
382-
// the host for EOS detection.
383-
auto decode_tokens = clone_tensor_ptr_to(decode_tokens_cpu, cuda_device);
393+
// The token input is the aliased on-device output (device_out_token); only
394+
// the position still needs a fixed device buffer refreshed by a per-round
395+
// H2D (one int64 / round).
384396
auto decode_pos = clone_tensor_ptr_to(decode_pos_cpu, cuda_device);
385397
#else
386-
auto decode_tokens = decode_tokens_cpu;
398+
// Non-CUDA (MLX) path: keep host token/pos buffers; the backend stages them
399+
// and the host samples from the returned logits.
400+
std::vector<int64_t> decode_token_data = {static_cast<int64_t>(cur_token)};
401+
auto decode_tokens = from_blob(
402+
decode_token_data.data(), {1, 1}, executorch::aten::ScalarType::Long);
387403
auto decode_pos = decode_pos_cpu;
388404
#endif
389405

390406
uint64_t prev_token = cur_token;
391407
bool hit_eos = eos_ids.find(cur_token) != eos_ids.end();
392408
for (int32_t step = 0; step < FLAGS_max_new_tokens && !hit_eos; step++) {
393-
decode_token_data[0] = static_cast<int64_t>(cur_token);
394409
decode_pos_data[0] = pos;
395410

396411
#ifdef EXECUTORCH_BUILD_CUDA
397-
// skip_h2d: refresh the device-resident token/pos buffers ourselves (the
398-
// backend no longer inserts the H2D copy). The prior step's sampled id
399-
// already came back to the host via read_token, so re-upload the 8-byte
400-
// token and position into the fixed device buffers.
401-
ET_CHECK_MSG(
402-
cudaMemcpy(
403-
decode_tokens->mutable_data_ptr(),
404-
decode_token_data.data(),
405-
sizeof(int64_t),
406-
cudaMemcpyHostToDevice) == cudaSuccess,
407-
"Failed to upload decode token H2D");
412+
// Token stays on device (aliased from the previous forward's output); only
413+
// the 8-byte position is uploaded each round. No token D2H->H2D round-trip.
408414
ET_CHECK_MSG(
409415
cudaMemcpy(
410416
decode_pos->mutable_data_ptr(),
411417
decode_pos_data.data(),
412418
sizeof(int64_t),
413419
cudaMemcpyHostToDevice) == cudaSuccess,
414420
"Failed to upload decode position H2D");
421+
#else
422+
decode_token_data[0] = static_cast<int64_t>(cur_token);
415423
#endif
416424

417425
std::vector<EValue> inputs;
426+
#ifdef EXECUTORCH_BUILD_CUDA
427+
inputs.push_back(EValue(device_out_token));
428+
#else
418429
inputs.push_back(EValue(decode_tokens));
430+
#endif
419431
inputs.push_back(EValue(decode_pos));
420432

421433
#ifdef EXECUTORCH_BUILD_CUDA
@@ -432,7 +444,10 @@ int main(int argc, char** argv) {
432444

433445
prev_token = cur_token;
434446
#ifdef EXECUTORCH_BUILD_CUDA
435-
cur_token = read_token(result.get()[0].toTensor());
447+
const auto& out_tensor = result.get()[0].toTensor();
448+
cur_token = read_token(out_tensor);
449+
// Alias this step's on-device output token as the next step's token input.
450+
device_out_token = make_tensor_ptr(out_tensor);
436451
#else
437452
cur_token = static_cast<uint64_t>(
438453
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)