use super::cache::{ForwardScratch, KvCache};
use super::detokenize::{IncrementalDetokenizer, decode_tokens};
use super::model::Qwen35Model;
use super::sampling::sample_token;
use crate::attention::gdn::GatedDeltaNetState;
use crate::error::InferenceError;
use crate::model::qwen35_config::{GenerateConfig, GenerateOutput, Qwen35Config};
use crate::tokenizer::common::Tokenizer;
impl Qwen35Model {
pub fn generate(
&self,
prompt: &str,
gen_cfg: &GenerateConfig,
) -> Result<GenerateOutput, InferenceError> {
let cfg = &self.config;
let mut rng_state = initial_rng_state(gen_cfg.seed);
let input = self.tokenizer.tokenize(prompt);
let prompt_ids: Vec<u32> = input.input_ids[..input.real_length].to_vec();
let prompt_len = prompt_ids.len();
if prompt_len == 0 {
return Err(InferenceError::Inference("empty prompt".into()));
}
if gen_cfg.max_new_tokens == 0 {
return Ok(GenerateOutput {
text: String::new(),
token_ids: vec![],
prompt_tokens: prompt_len,
generated_tokens: 0,
stopped: false,
});
}
let max_context = self.max_context();
if prompt_len.saturating_add(gen_cfg.max_new_tokens) > max_context {
return Err(InferenceError::Inference(format!(
"prompt ({prompt_len} tokens) plus max_new_tokens ({}) exceeds \
model context window ({max_context})",
gen_cfg.max_new_tokens
)));
}
let num_linear = cfg.num_linear_attention_layers();
let num_full = cfg.num_full_attention_layers();
let mut gdn_states: Vec<GatedDeltaNetState> = (0..num_linear)
.map(|_| GatedDeltaNetState::new(cfg))
.collect();
let mut kv_cache = KvCache::new(num_full);
let mut scratch = ForwardScratch::new();
let mut generated_ids: Vec<u32> = Vec::with_capacity(gen_cfg.max_new_tokens);
let mut all_ids = prompt_ids.clone();
prefill_tokens(
self,
&prompt_ids,
&mut gdn_states,
&mut kv_cache,
&mut scratch,
);
kv_cache.seq_len = prompt_len;
let next_id = sample_token(
&scratch.logits[..cfg.vocab_size],
gen_cfg,
&all_ids,
&mut rng_state,
);
if should_stop_token(cfg, gen_cfg, next_id) {
return Ok(GenerateOutput {
text: String::new(),
token_ids: vec![],
prompt_tokens: prompt_len,
generated_tokens: 0,
stopped: true,
});
}
generated_ids.push(next_id);
all_ids.push(next_id);
if gen_cfg.stop_strings.is_empty() {
let stopped = decode_loop(
self,
gen_cfg,
&mut all_ids,
&mut generated_ids,
&mut rng_state,
&mut gdn_states,
&mut kv_cache,
&mut scratch,
)?;
let text = decode_tokens(&self.tokenizer, &generated_ids);
Ok(GenerateOutput {
text,
token_ids: generated_ids.clone(),
prompt_tokens: prompt_len,
generated_tokens: generated_ids.len(),
stopped,
})
} else {
let mut detok = IncrementalDetokenizer::new();
let first_delta = detok.push(&self.tokenizer, next_id);
let mut full = first_delta;
if let Some(hit) = earliest_stop_match(&full, &gen_cfg.stop_strings) {
full.truncate(hit);
return Ok(GenerateOutput {
text: full,
token_ids: generated_ids.clone(),
prompt_tokens: prompt_len,
generated_tokens: generated_ids.len(),
stopped: true,
});
}
let stopped = decode_loop_with_stops(
self,
gen_cfg,
&mut all_ids,
&mut generated_ids,
&mut rng_state,
&mut gdn_states,
&mut kv_cache,
&mut scratch,
&mut detok,
&mut full,
)?;
Ok(GenerateOutput {
text: full,
token_ids: generated_ids.clone(),
prompt_tokens: prompt_len,
generated_tokens: generated_ids.len(),
stopped,
})
}
}
pub fn generate_streaming(
&self,
prompt: &str,
gen_cfg: &GenerateConfig,
mut on_token: impl FnMut(&str),
) -> Result<GenerateOutput, InferenceError> {
let cfg = &self.config;
let mut rng_state = initial_rng_state(gen_cfg.seed);
let input = self.tokenizer.tokenize(prompt);
let prompt_ids: Vec<u32> = input.input_ids[..input.real_length].to_vec();
let prompt_len = prompt_ids.len();
if prompt_len == 0 {
return Err(InferenceError::Inference("empty prompt".into()));
}
if gen_cfg.max_new_tokens == 0 {
return Ok(GenerateOutput {
text: String::new(),
token_ids: vec![],
prompt_tokens: prompt_len,
generated_tokens: 0,
stopped: false,
});
}
let max_context = self.max_context();
if prompt_len.saturating_add(gen_cfg.max_new_tokens) > max_context {
return Err(InferenceError::Inference(format!(
"prompt ({prompt_len} tokens) plus max_new_tokens ({}) exceeds \
model context window ({max_context})",
gen_cfg.max_new_tokens
)));
}
let num_linear = cfg.num_linear_attention_layers();
let num_full = cfg.num_full_attention_layers();
let mut gdn_states: Vec<GatedDeltaNetState> = (0..num_linear)
.map(|_| GatedDeltaNetState::new(cfg))
.collect();
let mut kv_cache = KvCache::new(num_full);
let mut scratch = ForwardScratch::new();
let mut generated_ids: Vec<u32> = Vec::with_capacity(gen_cfg.max_new_tokens);
let mut all_ids = prompt_ids.clone();
prefill_tokens(
self,
&prompt_ids,
&mut gdn_states,
&mut kv_cache,
&mut scratch,
);
kv_cache.seq_len = prompt_len;
let next_id = sample_token(
&scratch.logits[..cfg.vocab_size],
gen_cfg,
&all_ids,
&mut rng_state,
);
if should_stop_token(cfg, gen_cfg, next_id) {
return Ok(GenerateOutput {
text: String::new(),
token_ids: vec![],
prompt_tokens: prompt_len,
generated_tokens: 0,
stopped: true,
});
}
generated_ids.push(next_id);
all_ids.push(next_id);
let mut detok = IncrementalDetokenizer::new();
if gen_cfg.stop_strings.is_empty() {
let delta = detok.push(&self.tokenizer, next_id);
if !delta.is_empty() {
on_token(&delta);
}
let mut stopped = false;
for _ in 1..gen_cfg.max_new_tokens {
let pos = kv_cache.seq_len;
let Some(&last_token) = all_ids.last() else {
return Err(InferenceError::Inference("empty generation state".into()));
};
self.forward_step(
last_token,
pos,
&mut gdn_states,
&mut kv_cache,
&mut scratch,
);
kv_cache.seq_len += 1;
let next_id = sample_token(
&scratch.logits[..cfg.vocab_size],
gen_cfg,
&all_ids,
&mut rng_state,
);
if should_stop_token(cfg, gen_cfg, next_id) {
stopped = true;
break;
}
generated_ids.push(next_id);
all_ids.push(next_id);
let delta = detok.push(&self.tokenizer, next_id);
if !delta.is_empty() {
on_token(&delta);
}
}
let tail = detok.finish();
if !tail.is_empty() {
on_token(&tail);
}
Ok(GenerateOutput {
text: detok.text(),
token_ids: generated_ids.clone(),
prompt_tokens: prompt_len,
generated_tokens: generated_ids.len(),
stopped,
})
} else {
let mut streamer = StopStreamer::new(&gen_cfg.stop_strings);
let first_delta = detok.push(&self.tokenizer, next_id);
if streamer.push(&first_delta, &mut on_token) {
return Ok(GenerateOutput {
text: streamer.into_text(),
token_ids: generated_ids.clone(),
prompt_tokens: prompt_len,
generated_tokens: generated_ids.len(),
stopped: true,
});
}
let mut stopped = false;
for _ in 1..gen_cfg.max_new_tokens {
let pos = kv_cache.seq_len;
let Some(&last_token) = all_ids.last() else {
return Err(InferenceError::Inference("empty generation state".into()));
};
self.forward_step(
last_token,
pos,
&mut gdn_states,
&mut kv_cache,
&mut scratch,
);
kv_cache.seq_len += 1;
let next_id = sample_token(
&scratch.logits[..cfg.vocab_size],
gen_cfg,
&all_ids,
&mut rng_state,
);
if should_stop_token(cfg, gen_cfg, next_id) {
stopped = true;
break;
}
generated_ids.push(next_id);
all_ids.push(next_id);
let delta = detok.push(&self.tokenizer, next_id);
if streamer.push(&delta, &mut on_token) {
stopped = true;
break;
}
}
streamer.finish(&detok.finish(), &mut on_token);
stopped |= streamer.stopped;
Ok(GenerateOutput {
text: streamer.into_text(),
token_ids: generated_ids.clone(),
prompt_tokens: prompt_len,
generated_tokens: generated_ids.len(),
stopped,
})
}
}
}
fn initial_rng_state(seed: Option<u64>) -> u64 {
match seed {
Some(s) => {
if s == 0 {
1
} else {
s
}
}
None => {
use std::time::SystemTime;
let t = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0x12345678_9abcdef0);
if t == 0 { 1 } else { t }
}
}
}
fn prefill_tokens(
model: &Qwen35Model,
prompt_ids: &[u32],
gdn_states: &mut [GatedDeltaNetState],
kv_cache: &mut KvCache,
scratch: &mut ForwardScratch,
) {
let prompt_len = prompt_ids.len();
for (pos, &token_id) in prompt_ids.iter().enumerate() {
model.forward_step(token_id, pos, gdn_states, kv_cache, scratch);
if pos < prompt_len - 1 {
kv_cache.seq_len += 1;
}
}
}
pub(crate) struct StopStreamer<'a> {
full: String,
emitted: usize,
max_stop: usize,
stops: &'a [String],
stopped: bool,
}
impl<'a> StopStreamer<'a> {
pub(crate) fn new(stops: &'a [String]) -> Self {
let max_stop = stops.iter().map(String::len).max().unwrap_or(1);
Self {
full: String::new(),
emitted: 0,
max_stop,
stops,
stopped: false,
}
}
pub(crate) fn push(&mut self, delta: &str, sink: &mut impl FnMut(&str)) -> bool {
if delta.is_empty() {
return false;
}
self.full.push_str(delta);
if let Some(hit) = earliest_stop_match(&self.full, self.stops) {
let slice = &self.full[self.emitted..hit];
if !slice.is_empty() {
sink(slice);
}
self.full.truncate(hit);
self.emitted = self.full.len();
self.stopped = true;
return true;
}
let mut safe = self
.full
.len()
.saturating_sub(self.max_stop.saturating_sub(1));
safe = safe.max(self.emitted);
while safe > self.emitted && !self.full.is_char_boundary(safe) {
safe -= 1;
}
if safe > self.emitted {
sink(&self.full[self.emitted..safe]);
self.emitted = safe;
}
false
}
pub(crate) fn finish(&mut self, tail: &str, sink: &mut impl FnMut(&str)) {
if self.stopped {
return;
}
if !tail.is_empty() {
self.full.push_str(tail);
}
if let Some(hit) = earliest_stop_match(&self.full, self.stops) {
let slice = &self.full[self.emitted..hit];
if !slice.is_empty() {
sink(slice);
}
self.full.truncate(hit);
self.emitted = self.full.len();
self.stopped = true;
return;
}
if self.emitted < self.full.len() {
sink(&self.full[self.emitted..]);
self.emitted = self.full.len();
}
}
pub(crate) fn into_text(self) -> String {
self.full
}
}
fn decode_loop(
model: &Qwen35Model,
gen_cfg: &GenerateConfig,
all_ids: &mut Vec<u32>,
generated_ids: &mut Vec<u32>,
rng_state: &mut u64,
gdn_states: &mut [GatedDeltaNetState],
kv_cache: &mut KvCache,
scratch: &mut ForwardScratch,
) -> Result<bool, InferenceError> {
let cfg = &model.config;
for _ in 1..gen_cfg.max_new_tokens {
let pos = kv_cache.seq_len;
let Some(&last_token) = all_ids.last() else {
return Err(InferenceError::Inference("empty generation state".into()));
};
model.forward_step(last_token, pos, gdn_states, kv_cache, scratch);
kv_cache.seq_len += 1;
let next_id = sample_token(
&scratch.logits[..cfg.vocab_size],
gen_cfg,
all_ids,
rng_state,
);
if should_stop_token(cfg, gen_cfg, next_id) {
return Ok(true);
}
generated_ids.push(next_id);
all_ids.push(next_id);
}
Ok(false)
}
#[allow(clippy::too_many_arguments)]
fn decode_loop_with_stops(
model: &Qwen35Model,
gen_cfg: &GenerateConfig,
all_ids: &mut Vec<u32>,
generated_ids: &mut Vec<u32>,
rng_state: &mut u64,
gdn_states: &mut [GatedDeltaNetState],
kv_cache: &mut KvCache,
scratch: &mut ForwardScratch,
detok: &mut IncrementalDetokenizer,
full: &mut String,
) -> Result<bool, InferenceError> {
let cfg = &model.config;
let mut stopped = false;
for _ in 1..gen_cfg.max_new_tokens {
let pos = kv_cache.seq_len;
let Some(&last_token) = all_ids.last() else {
return Err(InferenceError::Inference("empty generation state".into()));
};
model.forward_step(last_token, pos, gdn_states, kv_cache, scratch);
kv_cache.seq_len += 1;
let next_id = sample_token(
&scratch.logits[..cfg.vocab_size],
gen_cfg,
all_ids,
rng_state,
);
if should_stop_token(cfg, gen_cfg, next_id) {
stopped = true;
break;
}
generated_ids.push(next_id);
all_ids.push(next_id);
let delta = detok.push(&model.tokenizer, next_id);
if !delta.is_empty() {
full.push_str(&delta);
}
if let Some(hit) = earliest_stop_match(full, &gen_cfg.stop_strings) {
full.truncate(hit);
stopped = true;
break;
}
}
if !stopped {
let tail = detok.finish();
if !tail.is_empty() {
full.push_str(&tail);
if let Some(hit) = earliest_stop_match(full, &gen_cfg.stop_strings) {
full.truncate(hit);
return Ok(true);
}
}
return Ok(false);
}
Ok(true)
}
pub(crate) fn earliest_stop_match(haystack: &str, stops: &[String]) -> Option<usize> {
stops.iter().filter_map(|s| haystack.find(s.as_str())).min()
}
pub fn should_stop_token(cfg: &Qwen35Config, gen_cfg: &GenerateConfig, token_id: u32) -> bool {
token_id == cfg.eos_token_id || gen_cfg.stop_token_ids.contains(&token_id)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn earliest_stop_match_single_present() {
assert_eq!(
earliest_stop_match("hello world", &["world".to_string()]),
Some(6)
);
}
#[test]
fn earliest_stop_match_no_match() {
assert_eq!(
earliest_stop_match("hello world", &["foo".to_string()]),
None
);
}
#[test]
fn earliest_stop_match_multiple_returns_earliest() {
assert_eq!(
earliest_stop_match("hello world", &["world".to_string(), "lo".to_string()]),
Some(3)
);
}
#[test]
fn earliest_stop_match_at_index_zero() {
assert_eq!(
earliest_stop_match("stopword rest", &["stop".to_string()]),
Some(0)
);
}
#[test]
fn earliest_stop_match_multibyte_utf8() {
assert_eq!(
earliest_stop_match("世界hello", &["界".to_string()]),
Some(3)
);
}
#[test]
fn earliest_stop_match_empty_stops() {
assert_eq!(earliest_stop_match("hello", &[]), None);
}
#[test]
fn generate_config_default_stop_strings_empty() {
let cfg = GenerateConfig::default();
assert!(cfg.stop_strings.is_empty());
}
#[test]
fn generate_config_stop_strings_field_explicit() {
let cfg = GenerateConfig {
stop_strings: vec!["</s>".to_string(), "\nUser:".to_string()],
..Default::default()
};
assert_eq!(cfg.stop_strings.len(), 2);
assert_eq!(cfg.stop_strings[0], "</s>");
}
#[test]
fn stop_streamer_stop_split_across_deltas_no_double_emit() {
let stops = vec!["World".to_string()];
let mut streamer = StopStreamer::new(&stops);
let mut all_emitted: Vec<String> = Vec::new();
let stopped1 = streamer.push("hel", &mut |s| all_emitted.push(s.to_string()));
assert!(!stopped1);
let stopped2 = streamer.push("lo W", &mut |s| all_emitted.push(s.to_string()));
assert!(!stopped2);
let stopped3 = streamer.push("orld!", &mut |s| all_emitted.push(s.to_string()));
assert!(stopped3, "stop should be detected on third delta");
let concatenated = all_emitted.join("");
assert_eq!(
concatenated, "hello ",
"emitted concat must equal pre-stop text exactly once (BUG 1 regression)"
);
assert_eq!(streamer.into_text(), "hello ");
}
#[test]
fn stop_streamer_stop_at_first_delta() {
let stops = vec!["Stop".to_string()];
let mut streamer = StopStreamer::new(&stops);
let mut emitted: Vec<String> = Vec::new();
let stopped = streamer.push("Stop now", &mut |s| emitted.push(s.to_string()));
assert!(stopped);
assert_eq!(emitted.join(""), "");
assert_eq!(streamer.into_text(), "");
}
#[test]
fn stop_streamer_no_stop_natural_end() {
let stops = vec!["zzz".to_string()];
let mut streamer = StopStreamer::new(&stops);
let mut emitted: Vec<String> = Vec::new();
streamer.push("abc", &mut |s| emitted.push(s.to_string()));
streamer.push("def", &mut |s| emitted.push(s.to_string()));
streamer.finish("", &mut |s| emitted.push(s.to_string()));
assert_eq!(emitted.join(""), "abcdef");
assert_eq!(streamer.into_text(), "abcdef");
}
#[test]
fn stop_streamer_multibyte_no_panic() {
let stops = vec!["STOP".to_string()];
let mut streamer = StopStreamer::new(&stops);
let mut emitted: Vec<String> = Vec::new();
streamer.push("世", &mut |s| emitted.push(s.to_string()));
streamer.push("界x", &mut |s| emitted.push(s.to_string()));
streamer.finish("", &mut |s| emitted.push(s.to_string()));
let concat = emitted.join("");
assert_eq!(concat, streamer.into_text());
}
#[test]
fn stop_streamer_stop_at_delta_boundary() {
let stops = vec!["STOP".to_string()];
let mut streamer = StopStreamer::new(&stops);
let mut emitted: Vec<String> = Vec::new();
let stopped1 = streamer.push("abc", &mut |s| emitted.push(s.to_string()));
assert!(!stopped1);
let stopped2 = streamer.push("STOP", &mut |s| emitted.push(s.to_string()));
assert!(stopped2);
assert_eq!(emitted.join(""), "abc");
assert_eq!(streamer.into_text(), "abc");
}
#[test]
fn stop_streamer_hold_back_emits_safe_prefix_then_finish() {
let stops = vec!["xyz".to_string()]; let mut streamer = StopStreamer::new(&stops);
let mut emitted: Vec<String> = Vec::new();
let stopped = streamer.push("abcde", &mut |s| emitted.push(s.to_string()));
assert!(!stopped);
assert_eq!(emitted.join(""), "abc");
streamer.finish("", &mut |s| emitted.push(s.to_string()));
assert_eq!(emitted.join(""), "abcde");
assert_eq!(streamer.into_text(), "abcde");
}
#[test]
fn stop_streamer_finish_noop_after_stop() {
let stops = vec!["END".to_string()];
let mut streamer = StopStreamer::new(&stops);
let mut emitted: Vec<String> = Vec::new();
let stopped = streamer.push("helloENDextra", &mut |s| emitted.push(s.to_string()));
assert!(stopped);
streamer.finish("should_not_appear", &mut |s| emitted.push(s.to_string()));
assert_eq!(emitted.join(""), "hello");
assert_eq!(streamer.into_text(), "hello");
}
}