@@ -635,6 +635,7 @@ std::optional<SessionId> MuseGlimmerCudaDFlashExecutor::clone(
635635#endif
636636 State copy{*target_id, *draft_id};
637637 copy.frontier = upto;
638+ copy.pending = upto == state.frontier ? state.pending : std::nullopt ;
638639 copy.hidden = state.hidden ;
639640 if (!df::trim_hidden (copy.hidden , row_bytes_, draft_position, upto)) {
640641 target_->cache_control ().seq_rm (*target_id);
@@ -680,6 +681,7 @@ bool MuseGlimmerCudaDFlashExecutor::rewind(State& state, int position) {
680681 return false ;
681682 }
682683 state.frontier = position;
684+ state.pending .reset ();
683685 return true ;
684686}
685687
@@ -887,7 +889,8 @@ bool MuseGlimmerCudaDFlashExecutor::draft(
887889
888890bool MuseGlimmerCudaDFlashExecutor::speculate (
889891 const batching::BatchInput& batch,
890- batching::BatchOutput& out) {
892+ batching::BatchOutput& out,
893+ bool pending_decode) {
891894 std::size_t cursor = 0 ;
892895 while (cursor < batch.inputs .size ()) {
893896 std::vector<SessionId> owners;
@@ -899,8 +902,9 @@ bool MuseGlimmerCudaDFlashExecutor::speculate(
899902 auto & state = sessions_.at (input.sid );
900903 const auto end =
901904 static_cast <std::int64_t >(input.position ) + input.offset + input.size ;
902- if (!out.outputs [cursor] || end != state.frontier ||
903- end > max_context_ - block_) {
905+ const int seed_rows = pending_decode ? 1 : 0 ;
906+ if (!out.outputs [cursor] || end != state.frontier + seed_rows ||
907+ end - seed_rows > max_context_ - block_) {
904908 ++cursor;
905909 continue ;
906910 }
@@ -917,7 +921,10 @@ bool MuseGlimmerCudaDFlashExecutor::speculate(
917921 }
918922 context += static_cast <int >(rows);
919923 owners.push_back (input.sid );
920- anchors.push_back (out.outputs [cursor]->tokens .front ());
924+ anchors.push_back (
925+ pending_decode ? (*std::get<batching::TokenInputPtr>(
926+ input.payload ))[input.offset ]
927+ : out.outputs [cursor]->tokens .front ());
921928 indices.push_back (cursor++);
922929 }
923930 if (owners.empty ()) {
@@ -982,9 +989,9 @@ bool MuseGlimmerCudaDFlashExecutor::speculate(
982989 return false ;
983990 }
984991 state.frontier = committed;
985- // The regular preamble 's prediction is new output, now committed by
986- // verification. Append only predictions after that anchor. The last
987- // prediction remains uncommitted : frontier = input.end + out.size() - 1.
992+ // Pending decode consumes Input 's already-emitted token; preamble mode
993+ // consumes its new anchor. Both append only new predictions, leaving
994+ // correction/bonus pending : frontier = input.end + out.size() - 1.
988995 auto & output_tokens = out.outputs [indices[i]]->tokens ;
989996 output_tokens.insert (
990997 output_tokens.end (), burst.tokens .begin (), burst.tokens .end ());
@@ -1009,6 +1016,7 @@ bool MuseGlimmerCudaDFlashExecutor::fail_batch(
10091016 failed_ = true ;
10101017 for (auto & item : sessions_) {
10111018 item.second .poisoned = true ;
1019+ item.second .pending .reset ();
10121020 if (fenced) {
10131021 item.second .hidden .clear ();
10141022 }
@@ -1045,6 +1053,7 @@ bool MuseGlimmerCudaDFlashExecutor::execute(
10451053 std::vector<SessionId> token_owners;
10461054 std::unordered_map<SessionId, int > cursor;
10471055 std::vector<std::pair<SessionId, int >> rewinds;
1056+ bool pending_decode = !batch.inputs .empty ();
10481057 for (const auto & input : batch.inputs ) {
10491058 const auto it = sessions_.find (input.sid );
10501059 const auto * payload =
@@ -1082,6 +1091,13 @@ bool MuseGlimmerCudaDFlashExecutor::execute(
10821091 if (start < at->second ) {
10831092 rewinds.emplace_back (input.sid , static_cast <int >(start));
10841093 }
1094+ // A one-token prompt/replay is not necessarily emitted decode feedback.
1095+ // Stay on the legacy path unless every owner has an exact pending seed,
1096+ // one input, and room for the complete trained draft block.
1097+ pending_decode = pending_decode && first && input.produce_output &&
1098+ input.size == 1 && start == it->second .frontier &&
1099+ start <= max_context_ - block_ && it->second .pending &&
1100+ *it->second .pending == (**payload)[input.offset ];
10851101 at->second = static_cast <int >(end);
10861102 for (std::size_t j = 0 ; j < input.size ; ++j) {
10871103 const auto token = (**payload)[input.offset + j];
@@ -1104,19 +1120,49 @@ bool MuseGlimmerCudaDFlashExecutor::execute(
11041120 return fail_batch (batch, out);
11051121 }
11061122 }
1107- std::vector<Token> predictions;
1108- if (!run_target (step, token_owners, predictions, true )) {
1123+ if (pending_decode) {
1124+ ++stats_.pending_decode_batches ;
1125+ stats_.pending_decode_owners += batch.inputs .size ();
1126+ for (std::size_t i = 0 ; i < batch.inputs .size (); ++i) {
1127+ out.outputs [i] = batching::Output{batch.inputs [i].sid , {}};
1128+ }
1129+ } else {
1130+ std::vector<Token> predictions;
1131+ if (!run_target (step, token_owners, predictions, true )) {
1132+ return fail_batch (batch, out);
1133+ }
1134+ for (std::size_t i = 0 ; i < batch.inputs .size (); ++i) {
1135+ sessions_.at (batch.inputs [i].sid ).pending .reset ();
1136+ if (batch.inputs [i].produce_output ) {
1137+ out.outputs [i] =
1138+ batching::Output{batch.inputs [i].sid , {predictions[i]}};
1139+ }
1140+ }
1141+ }
1142+ if (!speculate (batch, out, pending_decode)) {
11091143 return fail_batch (batch, out);
11101144 }
11111145 for (std::size_t i = 0 ; i < batch.inputs .size (); ++i) {
1112- if (batch.inputs [i].produce_output ) {
1113- out.outputs [i] =
1114- batching::Output{batch.inputs [i].sid , {predictions[i]}};
1146+ if (out.outputs [i]) {
1147+ const auto & tokens = out.outputs [i]->tokens ;
1148+ const auto & input = batch.inputs [i];
1149+ auto & state = sessions_.at (input.sid );
1150+ const auto end = static_cast <std::int64_t >(input.position ) +
1151+ input.offset + input.size ;
1152+ if (tokens.empty ()) {
1153+ return fail_batch (batch, out);
1154+ }
1155+ const auto expected =
1156+ end + static_cast <std::int64_t >(tokens.size ()) - 1 ;
1157+ if (pending_decode && expected != state.frontier ) {
1158+ return fail_batch (batch, out);
1159+ }
1160+ // Only the final input for a repeated owner may seed the next cycle.
1161+ if (expected == state.frontier ) {
1162+ state.pending = tokens.back ();
1163+ }
11151164 }
11161165 }
1117- if (!speculate (batch, out)) {
1118- return fail_batch (batch, out);
1119- }
11201166 ++stats_.executions ;
11211167 return true ;
11221168#if ET_HAS_EXCEPTIONS
0 commit comments