@@ -57,11 +57,18 @@ constexpr const char* kDecodeMethod = "decode";
5757// `forward` method was compiled for. Read into the metadata map in create().
5858constexpr const char * kMaxPrefillChunk = " get_max_prefill_chunk" ;
5959
60+ // Constant method (MLX --sample exports) signalling that `forward` samples a
61+ // token id on-device and returns it directly. Read into the metadata map in
62+ // create().
63+ constexpr const char * kUseSampling = " use_sampling" ;
64+
6065Result<uint64_t > read_sampled_token (
6166 const executorch::aten::Tensor& output,
62- float temperature) {
67+ float temperature,
68+ bool use_sampling) {
6369#ifdef EXECUTORCH_BUILD_CUDA
6470 (void )temperature;
71+ (void )use_sampling;
6572 const void * ptr = output.const_data_ptr ();
6673 cudaPointerAttributes attrs;
6774 const bool on_device = cudaPointerGetAttributes (&attrs, ptr) == cudaSuccess &&
@@ -78,6 +85,11 @@ Result<uint64_t> read_sampled_token(
7885 }
7986 return static_cast <uint64_t >(llrintf (val));
8087#else
88+ // On-device sampling (MLX --sample): `forward` already returns the int64
89+ // token id, so read it directly instead of sampling host-side from logits.
90+ if (use_sampling) {
91+ return static_cast <uint64_t >(output.const_data_ptr <int64_t >()[0 ]);
92+ }
8193 return static_cast <uint64_t >(
8294 logits_to_token (output, temperature < 0 .0f ? 0 .0f : temperature));
8395#endif
@@ -208,6 +220,19 @@ class Qwen35MoESession : public LLMSession {
208220#ifdef EXECUTORCH_BUILD_CUDA
209221 temp_tensor_ =
210222 from_blob (&temp_val_, {1 }, executorch::aten::ScalarType::Float);
223+ #endif
224+ #ifdef EXECUTORCH_BUILD_MLX
225+ if (auto it = metadata_.find (kUseSampling ); it != metadata_.end ()) {
226+ use_sampling_ = it->second != 0 ;
227+ }
228+ // 0-dim scalar inputs appended to `forward` when sampling on-device. Reused
229+ // across calls; only the backing scalar is rewritten per token.
230+ temp_tensor_mlx_ =
231+ from_blob (&temp_val_mlx_, {}, executorch::aten::ScalarType::Float);
232+ top_p_tensor_ =
233+ from_blob (&top_p_val_, {}, executorch::aten::ScalarType::Float);
234+ seed_tensor_ =
235+ from_blob (&seed_val_, {}, executorch::aten::ScalarType::Long);
211236#endif
212237 }
213238
@@ -231,15 +256,27 @@ class Qwen35MoESession : public LLMSession {
231256 }
232257 float first_token_temp = temperature_;
233258 if (initial_sampling != nullptr ) {
234- if (initial_sampling->top_p != 1 .0f || initial_sampling->top_k != 0 ||
235- initial_sampling->seed != 0 ) {
259+ if (initial_sampling->top_k != 0 ) {
260+ ET_LOG (Error, " prefill_tokens: top_k is not implemented" );
261+ return Error::NotSupported;
262+ }
263+ if (!use_sampling_ &&
264+ (initial_sampling->top_p != 1 .0f || initial_sampling->seed != 0 )) {
236265 ET_LOG (
237266 Error,
238- " prefill_tokens: only temperature is supported; top_p/top_k/seed "
239- " are not yet implemented " );
267+ " prefill_tokens: top_p/seed require a sampling model "
268+ " (export with --sample); only temperature is supported otherwise " );
240269 return Error::NotSupported;
241270 }
242271 first_token_temp = initial_sampling->temperature ;
272+ if (use_sampling_) {
273+ if (!valid_top_p (initial_sampling->top_p )) {
274+ ET_LOG (Error, " prefill_tokens: top_p must be in (0, 1]" );
275+ return Error::InvalidArgument;
276+ }
277+ top_p_ = initial_sampling->top_p ;
278+ seed_ = initial_sampling->seed ; // base seed for the kept (token 0) draw
279+ }
243280 }
244281 if (!valid_temperature (first_token_temp)) {
245282 ET_LOG (Error, " prefill_tokens: temperature must be -1 or in [0, 1]" );
@@ -307,6 +344,14 @@ class Qwen35MoESession : public LLMSession {
307344#ifdef EXECUTORCH_BUILD_CUDA
308345 set_temp (first_token_temp);
309346 inputs.push_back (EValue (temp_tensor_));
347+ #endif
348+ #ifdef EXECUTORCH_BUILD_MLX
349+ if (use_sampling_) {
350+ set_sampling_inputs (first_token_temp, top_p_, seed_);
351+ inputs.push_back (EValue (temp_tensor_mlx_));
352+ inputs.push_back (EValue (top_p_tensor_));
353+ inputs.push_back (EValue (seed_tensor_));
354+ }
310355#endif
311356 auto sampled =
312357 run_locked (method, inputs, first_token_temp, /* sync_after=*/ true );
@@ -316,15 +361,24 @@ class Qwen35MoESession : public LLMSession {
316361 }
317362 pending_ = sampled_token;
318363 prev_decode_token_.reset ();
364+ #ifdef EXECUTORCH_BUILD_MLX
365+ if (use_sampling_) {
366+ seed_ += 1 ; // first decode token draws with base+1
367+ }
368+ #endif
319369 return Error::Ok;
320370 }
321371
322372 Result<DecodeResult> decode_one (const SamplingConfig& sampling) override {
323- if (sampling.top_p != 1 .0f || sampling.top_k != 0 || sampling.seed != 0 ) {
373+ if (sampling.top_k != 0 ) {
374+ ET_LOG (Error, " Qwen35MoESession: top_k is not implemented" );
375+ return Error::NotSupported;
376+ }
377+ if (!use_sampling_ && (sampling.top_p != 1 .0f || sampling.seed != 0 )) {
324378 ET_LOG (
325379 Error,
326- " Qwen35MoESession: only temperature is supported; top_p/top_k/seed "
327- " are not implemented " );
380+ " Qwen35MoESession: top_p/seed require a sampling model "
381+ " (export with --sample); only temperature is supported otherwise " );
328382 return Error::NotSupported;
329383 }
330384 if (!valid_temperature (sampling.temperature )) {
@@ -336,6 +390,13 @@ class Qwen35MoESession : public LLMSession {
336390 InvalidState,
337391 " decode_one requires a pending token; call prefill_tokens() first" );
338392 temperature_ = sampling.temperature ;
393+ if (use_sampling_) {
394+ if (!valid_top_p (sampling.top_p )) {
395+ ET_LOG (Error, " decode_one: top_p must be in (0, 1]" );
396+ return Error::InvalidArgument;
397+ }
398+ top_p_ = sampling.top_p ;
399+ }
339400
340401 if (stop_.load (std::memory_order_relaxed)) {
341402 return DecodeResult{0 , " " , /* is_eos=*/ false , /* is_terminal=*/ true };
@@ -380,13 +441,26 @@ class Qwen35MoESession : public LLMSession {
380441#ifdef EXECUTORCH_BUILD_CUDA
381442 set_temp (temperature_);
382443 inputs.push_back (EValue (temp_tensor_));
444+ #endif
445+ #ifdef EXECUTORCH_BUILD_MLX
446+ if (use_sampling_) {
447+ set_sampling_inputs (temperature_, top_p_, seed_);
448+ inputs.push_back (EValue (temp_tensor_mlx_));
449+ inputs.push_back (EValue (top_p_tensor_));
450+ inputs.push_back (EValue (seed_tensor_));
451+ }
383452#endif
384453 auto sampled =
385454 run_locked (kDecodeMethod , inputs, temperature_, /* sync_after=*/ false );
386455 ET_CHECK_OK_OR_RETURN_ERROR (sampled.error ());
387456 pending_ = sampled.get ();
388457 prev_decode_token_ = token;
389458 pos_ += 1 ;
459+ #ifdef EXECUTORCH_BUILD_MLX
460+ if (use_sampling_) {
461+ seed_ += 1 ; // next token draws with the next seed in the schedule
462+ }
463+ #endif
390464 return DecodeResult{
391465 token, std::move (text_piece), /* is_eos=*/ false , /* is_terminal=*/ false };
392466 }
@@ -412,12 +486,26 @@ class Qwen35MoESession : public LLMSession {
412486 return temperature == -1 .0f || (temperature >= 0 .0f && temperature <= 1 .0f );
413487 }
414488
489+ static bool valid_top_p (float top_p) {
490+ return top_p > 0 .0f && top_p <= 1 .0f ;
491+ }
492+
415493#ifdef EXECUTORCH_BUILD_CUDA
416494 void set_temp (float t) {
417495 temp_val_ = (t <= 0 .0f ) ? 1e-6f : t;
418496 }
419497#endif
420498
499+ #ifdef EXECUTORCH_BUILD_MLX
500+ // Rewrite the backing scalars for the reused 0-dim sampling input tensors.
501+ // The -1 temperature sentinel maps to 0 (greedy).
502+ void set_sampling_inputs (float temp, float top_p, uint64_t seed) {
503+ temp_val_mlx_ = (temp < 0 .0f ) ? 0 .0f : temp;
504+ top_p_val_ = top_p;
505+ seed_val_ = static_cast <int64_t >(seed);
506+ }
507+ #endif
508+
421509 Result<uint64_t > run_locked (
422510 const char * method,
423511 std::vector<EValue>& inputs,
@@ -434,7 +522,8 @@ class Qwen35MoESession : public LLMSession {
434522 auto res = module_->execute (method, inputs);
435523#endif
436524 ET_CHECK_OK_OR_RETURN_ERROR (res.error ());
437- auto sampled = read_sampled_token (res.get ()[0 ].toTensor (), temperature);
525+ auto sampled =
526+ read_sampled_token (res.get ()[0 ].toTensor (), temperature, use_sampling_);
438527 ET_CHECK_OK_OR_RETURN_ERROR (sampled.error ());
439528#ifdef EXECUTORCH_BUILD_CUDA
440529 if (sync_after && cudaDeviceSynchronize () != cudaSuccess) {
@@ -460,6 +549,13 @@ class Qwen35MoESession : public LLMSession {
460549 float temperature_ = -1 .0f ;
461550 std::atomic<bool > stop_{false };
462551
552+ // On-device sampling state (MLX --sample). top_p_/seed_ are engine-managed:
553+ // the seed increments per generated token for decorrelated, reproducible
554+ // draws.
555+ bool use_sampling_ = false ;
556+ float top_p_ = 1 .0f ;
557+ uint64_t seed_ = 0 ;
558+
463559 int64_t decode_token_data_[1 ] = {0 };
464560 int64_t decode_pos_data_[1 ] = {0 };
465561 TensorPtr decode_tokens_;
@@ -472,6 +568,14 @@ class Qwen35MoESession : public LLMSession {
472568 float temp_val_ = 1e-6f ;
473569 TensorPtr temp_tensor_;
474570#endif
571+ #ifdef EXECUTORCH_BUILD_MLX
572+ float temp_val_mlx_ = 0 .0f ;
573+ float top_p_val_ = 1 .0f ;
574+ int64_t seed_val_ = 0 ;
575+ TensorPtr temp_tensor_mlx_;
576+ TensorPtr top_p_tensor_;
577+ TensorPtr seed_tensor_;
578+ #endif
475579};
476580
477581} // namespace
@@ -513,6 +617,11 @@ Result<std::unique_ptr<Qwen35MoEEngine>> Qwen35MoEEngine::create(
513617 if (auto mpc = meta_module->get (kMaxPrefillChunk ); mpc.ok ()) {
514618 metadata_result.get ()[kMaxPrefillChunk ] = mpc->toScalar ().to <int64_t >();
515619 }
620+ // get_llm_metadata doesn't harvest use_sampling; surface it so the session
621+ // knows `forward` returns a token id (on-device) rather than logits.
622+ if (auto us = meta_module->get (kUseSampling ); us.ok ()) {
623+ metadata_result.get ()[kUseSampling ] = us->toScalar ().to <int64_t >();
624+ }
516625#endif
517626 auto eos_ids = get_eos_ids (tokenizer.get (), meta_module.get ());
518627 // This export's metadata doesn't carry the chat-turn EOS (config.json has no
0 commit comments