Skip to content

Commit 54aea1f

Browse files
committed
Gemma MLX: reuse SamplingHead for on-device sampling; wire runtime top-k
1 parent 94eefdc commit 54aea1f

4 files changed

Lines changed: 37 additions & 35 deletions

File tree

examples/models/gemma4_31b/export.py

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -309,24 +309,6 @@ def _export_cuda(
309309
print("Done.")
310310

311311

312-
class _MLXSampleWrapper(nn.Module):
313-
"""Wrap the model so ``forward`` returns a sampled token id.
314-
315-
The MLX source transforms make ``forward`` return last-token logits
316-
``(B, vocab)``, so sample directly. Temperature, top_p, and seed are runtime
317-
scalar inputs so the same .pte serves any sampling request; the runner
318-
increments the seed per token.
319-
"""
320-
321-
def __init__(self, model: nn.Module):
322-
super().__init__()
323-
self.model = model
324-
325-
def forward(self, tokens, input_pos, temperature, top_p, seed):
326-
logits = self.model(tokens, input_pos)
327-
return torch.ops.mlx.sample(logits, temperature, top_p, seed)
328-
329-
330312
def _export_mlx(
331313
model: Gemma4_31B,
332314
config: Gemma4_31BConfig,
@@ -383,15 +365,23 @@ def _export_mlx(
383365
example_tokens = torch.tensor([[0, 1]], dtype=torch.long)
384366
example_input_pos = torch.tensor([0, 1], dtype=torch.long)
385367
if sample:
386-
model = _MLXSampleWrapper(model)
368+
# forward(tokens, input_pos, temperature, top_k, top_p, seed) -> token id.
369+
# gemma's MLX forward already returns last-token logits (B, vocab), so
370+
# SamplingHead is used directly with no per-model wrapper.
371+
from executorch.backends.mlx.llm.sampling import SamplingHead
372+
373+
model = SamplingHead(model)
387374
example_args = (
388375
example_tokens,
389376
example_input_pos,
390377
torch.tensor(1.0, dtype=torch.float32),
378+
torch.tensor(torch.iinfo(torch.int64).max, dtype=torch.int64),
391379
torch.tensor(1.0, dtype=torch.float32),
392380
torch.tensor(0, dtype=torch.int64),
393381
)
394-
dynamic_shapes = ({1: seq_dim}, {0: seq_dim}, None, None, None)
382+
# SamplingHead.forward takes ``*args``; dynamic_shapes mirrors that single
383+
# variadic parameter as one nested tuple over the positional inputs.
384+
dynamic_shapes = (({1: seq_dim}, {0: seq_dim}, None, None, None, None),)
395385
else:
396386
example_args = (example_tokens, example_input_pos)
397387
dynamic_shapes = ({1: seq_dim}, {0: seq_dim})

examples/models/gemma4_31b/gemma4_31b_engine.cpp

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,8 @@ class Gemma4_31BSession : public LLMSession {
304304
}
305305
temp_tensor_mlx_ =
306306
from_blob(&temp_val_mlx_, {}, executorch::aten::ScalarType::Float);
307+
top_k_tensor_ =
308+
from_blob(&top_k_val_, {}, executorch::aten::ScalarType::Long);
307309
top_p_tensor_ =
308310
from_blob(&top_p_val_, {}, executorch::aten::ScalarType::Float);
309311
seed_tensor_ =
@@ -329,15 +331,12 @@ class Gemma4_31BSession : public LLMSession {
329331
}
330332
float first_token_temp = temperature_;
331333
if (initial_sampling != nullptr) {
332-
if (initial_sampling->top_k != 0) {
333-
ET_LOG(Error, "prefill_tokens: top_k is not implemented");
334-
return Error::NotSupported;
335-
}
336334
if (!use_sampling_ &&
337-
(initial_sampling->top_p != 1.0f || initial_sampling->seed != 0)) {
335+
(initial_sampling->top_k != 0 || initial_sampling->top_p != 1.0f ||
336+
initial_sampling->seed != 0)) {
338337
ET_LOG(
339338
Error,
340-
"prefill_tokens: top_p/seed require a sampling model "
339+
"prefill_tokens: top_k/top_p/seed require a sampling model "
341340
"(export with --sample); only temperature is supported otherwise");
342341
return Error::NotSupported;
343342
}
@@ -347,6 +346,7 @@ class Gemma4_31BSession : public LLMSession {
347346
ET_LOG(Error, "prefill_tokens: top_p must be in (0, 1]");
348347
return Error::InvalidArgument;
349348
}
349+
top_k_ = initial_sampling->top_k;
350350
top_p_ = initial_sampling->top_p;
351351
seed_ = initial_sampling->seed;
352352
}
@@ -398,14 +398,11 @@ class Gemma4_31BSession : public LLMSession {
398398
}
399399

400400
Result<DecodeResult> decode_one(const SamplingConfig& sampling) override {
401-
if (sampling.top_k != 0) {
402-
ET_LOG(Error, "Gemma4_31BSession: top_k is not implemented");
403-
return Error::NotSupported;
404-
}
405-
if (!use_sampling_ && (sampling.top_p != 1.0f || sampling.seed != 0)) {
401+
if (!use_sampling_ &&
402+
(sampling.top_k != 0 || sampling.top_p != 1.0f || sampling.seed != 0)) {
406403
ET_LOG(
407404
Error,
408-
"Gemma4_31BSession: top_p/seed require a sampling model "
405+
"Gemma4_31BSession: top_k/top_p/seed require a sampling model "
409406
"(export with --sample); only temperature is supported otherwise");
410407
return Error::NotSupported;
411408
}
@@ -423,6 +420,7 @@ class Gemma4_31BSession : public LLMSession {
423420
ET_LOG(Error, "decode_one: top_p must be in (0, 1]");
424421
return Error::InvalidArgument;
425422
}
423+
top_k_ = sampling.top_k;
426424
top_p_ = sampling.top_p;
427425
}
428426

@@ -474,8 +472,9 @@ class Gemma4_31BSession : public LLMSession {
474472
inputs.push_back(EValue(decode_pos_));
475473
#ifdef EXECUTORCH_BUILD_MLX
476474
if (use_sampling_) {
477-
set_sampling_inputs(temperature_, top_p_, seed_);
475+
set_sampling_inputs(temperature_, top_k_, top_p_, seed_);
478476
inputs.push_back(EValue(temp_tensor_mlx_));
477+
inputs.push_back(EValue(top_k_tensor_));
479478
inputs.push_back(EValue(top_p_tensor_));
480479
inputs.push_back(EValue(seed_tensor_));
481480
}
@@ -522,8 +521,10 @@ class Gemma4_31BSession : public LLMSession {
522521
}
523522

524523
#ifdef EXECUTORCH_BUILD_MLX
525-
void set_sampling_inputs(float temp, float top_p, uint64_t seed) {
524+
void
525+
set_sampling_inputs(float temp, int64_t top_k, float top_p, uint64_t seed) {
526526
temp_val_mlx_ = (temp < 0.0f) ? 0.0f : temp;
527+
top_k_val_ = (top_k <= 0) ? INT64_MAX : top_k; // 0/neg = keep all
527528
top_p_val_ = top_p;
528529
seed_val_ = static_cast<int64_t>(seed);
529530
}
@@ -564,8 +565,9 @@ class Gemma4_31BSession : public LLMSession {
564565
#endif
565566
#ifdef EXECUTORCH_BUILD_MLX
566567
if (use_sampling_) {
567-
set_sampling_inputs(temperature, top_p_, seed_);
568+
set_sampling_inputs(temperature, top_k_, top_p_, seed_);
568569
inputs.push_back(EValue(temp_tensor_mlx_));
570+
inputs.push_back(EValue(top_k_tensor_));
569571
inputs.push_back(EValue(top_p_tensor_));
570572
inputs.push_back(EValue(seed_tensor_));
571573
}
@@ -705,6 +707,7 @@ class Gemma4_31BSession : public LLMSession {
705707
std::atomic<bool> stop_{false};
706708

707709
bool use_sampling_ = false;
710+
int64_t top_k_ = 0; // 0 = off (keep all); mapped to INT64_MAX on-device
708711
float top_p_ = 1.0f;
709712
uint64_t seed_ = 0;
710713

@@ -727,9 +730,11 @@ class Gemma4_31BSession : public LLMSession {
727730
#endif
728731
#ifdef EXECUTORCH_BUILD_MLX
729732
float temp_val_mlx_ = 0.0f;
733+
int64_t top_k_val_ = INT64_MAX;
730734
float top_p_val_ = 1.0f;
731735
int64_t seed_val_ = 0;
732736
TensorPtr temp_tensor_mlx_;
737+
TensorPtr top_k_tensor_;
733738
TensorPtr top_p_tensor_;
734739
TensorPtr seed_tensor_;
735740
#endif

examples/models/gemma4_31b/main.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ DEFINE_string(
5050
"",
5151
"Path to file containing prompt text (overrides --prompt).");
5252
DEFINE_double(temperature, 0.8, "Sampling temperature (0 = near-greedy).");
53+
DEFINE_int32(
54+
top_k,
55+
0,
56+
"Top-k sampling; keep the k most likely tokens. 0 (default) = off (keep "
57+
"all). Requires a model exported with --sample (MLX on-device sampling).");
5358
DEFINE_double(
5459
top_p,
5560
1.0,
@@ -174,6 +179,7 @@ int main(int argc, char** argv) {
174179

175180
llm::SamplingConfig sampling;
176181
sampling.temperature = static_cast<float>(FLAGS_temperature);
182+
sampling.top_k = FLAGS_top_k;
177183
sampling.top_p = static_cast<float>(FLAGS_top_p);
178184
// Only a --sample model uses the seed; randomize an unset seed for those and
179185
// leave non-sample models at 0 so they don't trip the top_p/seed guard.

examples/models/gemma4_31b/tests/test_mlx_pipeline.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ def sample(seed):
283283
tokens,
284284
input_pos,
285285
torch.tensor(0.8, dtype=torch.float32),
286+
torch.tensor(torch.iinfo(torch.int64).max, dtype=torch.int64),
286287
torch.tensor(0.9, dtype=torch.float32),
287288
torch.tensor(seed, dtype=torch.int64),
288289
]

0 commit comments

Comments
 (0)