fn qwen35_generate(
served: &crate::api::Qwen35Served,
prompt: &[u32],
config: &crate::gguf::QuantizedGenerateConfig,
top_n: Option<usize>,
on_token: &mut dyn FnMut(u32, Option<crate::gguf::logprobs::StepLogprobs>) -> bool,
) -> Result<(crate::session::Turn, Instant), String> {
let mut s = served.session.lock().map_err(|_| POISONED.to_string())?;
let turn = s.generate_with_logprobs(prompt, config, top_n, &mut |token, record| {
config.stop_tokens.contains(&token) || on_token(token, record)
});
let decode_ended = Instant::now();
served
.on_gpu
.store(s.on_gpu(), std::sync::atomic::Ordering::Relaxed);
turn.map(|turn| (turn, decode_ended))
.map_err(|e| e.to_string())
}
struct Qwen35Reply {
tokens: Vec<u32>,
steps: Vec<crate::gguf::logprobs::StepLogprobs>,
used_gpu: bool,
measured: TracedTurn,
phases: crate::api::PhaseTimings,
}
fn qwen35_turn(
served: &crate::api::Qwen35Served,
prompt: &[u32],
config: &crate::gguf::QuantizedGenerateConfig,
top_n: Option<usize>,
mut tracer: Option<crate::inference_trace::InferenceTracer>,
time_layers: bool,
) -> Result<Qwen35Reply, String> {
let mut s = served.session.lock().map_err(|_| POISONED.to_string())?;
let trace = tracer.as_mut().map(|tracer| crate::session::TurnTrace {
tracer,
layers: time_layers,
});
let mut steps = Vec::new();
let turn = s.generate_traced_with_logprobs(
prompt,
config,
top_n,
&mut |token, record| {
if !config.stop_tokens.contains(&token) {
steps.extend(record);
}
true
},
trace,
);
let decode_ended = Instant::now();
let layers = if time_layers {
s.take_layer_timings()
} else {
None
};
let measured = TracedTurn {
tracer,
layers,
num_layers: s.num_layers(),
};
served
.on_gpu
.store(s.on_gpu(), std::sync::atomic::Ordering::Relaxed);
let turn = turn.map_err(|e| e.to_string())?;
let mut reply = turn.tokens[prompt.len()..].to_vec();
if reply.last().is_some_and(|t| config.stop_tokens.contains(t)) {
reply.pop();
}
if top_n.is_some() && steps.len() != reply.len() {
return Err(format!(
"the reply's {} tokens came with {} logprobs records (#4971)",
reply.len(),
steps.len()
));
}
Ok(Qwen35Reply {
tokens: reply,
steps,
used_gpu: turn.used_gpu,
measured,
phases: crate::api::PhaseTimings::from_marks(
turn.prefill_started,
turn.prefill_ended,
decode_ended,
),
})
}
fn spawn_streaming_generate(
session: Arc<crate::api::Qwen35Served>,
input_ids: Vec<u32>,
gen_config: crate::gguf::QuantizedGenerateConfig,
sink_metrics: Arc<crate::metrics::MetricsCollector>,
top_n: Option<usize>,
) -> (
tokio::sync::mpsc::Receiver<Result<u32, String>>,
Option<super::chat_logprobs::StreamLogprobs>,
tokio::sync::oneshot::Receiver<crate::api::PhaseTimings>,
) {
let (tx, rx) = tokio::sync::mpsc::channel::<Result<u32, String>>(64);
let (timing_tx, timing_rx) = tokio::sync::oneshot::channel::<crate::api::PhaseTimings>();
let (records, logprobs) = top_n
.map(|_| super::chat_logprobs::StreamLogprobs::channel())
.unzip();
tokio::task::spawn_blocking(move || {
let mut sink = crate::api::openai_handlers::streaming_token_sink(tx.clone(), sink_metrics);
let result = qwen35_generate(
&session,
&input_ids,
&gen_config,
top_n,
&mut |t, record| {
if let (Some(records), Some(record)) = (&records, record) {
let _ = records.send(record);
}
sink(t)
},
);
match result {
Ok((turn, decode_ended)) => {
let _ = timing_tx.send(crate::api::PhaseTimings::from_marks(
turn.prefill_started,
turn.prefill_ended,
decode_ended,
));
},
Err(e) => {
let _ = tx.blocking_send(Err(e));
},
}
});
(rx, logprobs, timing_rx)
}
fn gen_config_from_request(
request: &ChatCompletionRequest,
budget: usize,
stop_tokens: Vec<u32>,
cancel: CancelToken,
) -> crate::gguf::QuantizedGenerateConfig {
use crate::gguf::QuantizedGenerateConfig;
let defaults = QuantizedGenerateConfig::default();
QuantizedGenerateConfig {
max_tokens: budget,
temperature: request.temperature.unwrap_or(defaults.temperature),
top_k: request.top_k.unwrap_or(defaults.top_k),
top_p: request.top_p.unwrap_or(defaults.top_p),
repeat_penalty: request.repeat_penalty.unwrap_or(defaults.repeat_penalty),
repeat_last_n: request.repeat_last_n.unwrap_or(defaults.repeat_last_n),
seed: request.seed.unwrap_or(defaults.seed),
stop_tokens,
cancel,
..defaults
}
}
async fn try_qwen35_backend(
state: &AppState,
request: &ChatCompletionRequest,
request_id: &str,
trace_level: Option<&str>,
start: Instant,
cancel: &CancelToken,
) -> Option<Response> {
let session = state.qwen35_session()?;
let Some(mapped) = state.mapped_gguf_model() else {
return Some(fail_response(
state,
StatusCode::INTERNAL_SERVER_ERROR,
"the Qwen3.5 session has no retained GGUF to tokenize with (#3571)",
));
};
let tokenizer = match require_tokenizer(state) {
Ok(t) => t,
Err(r) => return Some(r),
};
let architecture = state.model_architecture();
let prompt_text =
match crate::api::realize_handlers::format_chat_messages_official_thinking_tools(
Some(&mapped.model),
&request.messages,
architecture.as_deref(),
request.thinking(),
request.tools.as_deref(),
) {
Ok(p) => p,
Err(e) => return Some(fail_response(state, StatusCode::BAD_REQUEST, e.to_string())),
};
let input_ids = mapped.model.encode(&prompt_text).unwrap_or_default();
if input_ids.is_empty() {
return Some(fail_response(
state,
StatusCode::BAD_REQUEST,
"Messages cannot be empty",
));
}
let prompt_token_count = input_ids.len();
let context_length = session.context_length;
if prompt_token_count >= context_length {
return Some(fail_response(
state,
StatusCode::BAD_REQUEST,
format!(
"the prompt is {prompt_token_count} tokens and this model declares a context of \
{context_length}: it was refused whole rather than truncated (#3571)"
),
));
}
let max_tokens = request.max_tokens.unwrap_or(256);
let budget = max_tokens.min(context_length - prompt_token_count);
let stop_tokens = stop_tokens_unless_ignore_eos(request, state.model_eos_token_id());
let gen_config = gen_config_from_request(request, budget, stop_tokens, cancel.clone());
let top_n = request.logprobs_top_n();
if request.stream {
let (rx, logprobs, timing_rx) =
spawn_streaming_generate(session, input_ids, gen_config, state.metrics.clone(), top_n);
return Some(crate::api::openai_handlers::live_sse_response(
rx,
tokenizer,
request_id.to_string(),
request.model.clone(),
state.metrics.clone(),
start,
budget,
prompt_token_count,
Some(timing_rx),
request.stop.as_deref(),
crate::api::stream_tool_calls::StreamTools::from_request(request),
logprobs,
));
}
let decode_mapped = mapped.clone();
let tracer = crate::api::serve_trace::tracer_for(trace_level);
let time_layers = trace_level == Some("layer");
let turn = tokio::task::spawn_blocking(move || {
qwen35_turn(
&session,
&input_ids,
&gen_config,
top_n,
tracer,
time_layers,
)
})
.await;
let reply = match turn {
Ok(Ok(turn)) => turn,
Ok(Err(e)) => {
state.metrics.record_failure();
return Some(fail_response(
state,
StatusCode::INTERNAL_SERVER_ERROR,
format!("Qwen3.5 generation failed: {e}"),
));
},
Err(e) => {
state.metrics.record_failure();
return Some(fail_response(
state,
StatusCode::INTERNAL_SERVER_ERROR,
format!("Qwen3.5 generation task failed: {e}"),
));
},
};
let completion_tokens = reply.tokens.len();
let response_text = clean_chat_output(&decode_mapped.model.decode(&reply.tokens));
let logprobs = top_n.map(|_| super::ChatLogprobs::from_steps(&tokenizer, &reply.steps));
let measured = reply.measured;
let timings = reply
.phases
.to_timings_at(prompt_token_count, completion_tokens, start, None);
let duration = start.elapsed();
state.metrics.record_success(completion_tokens, duration);
let traces = crate::api::serve_trace::traces_for(&crate::api::serve_trace::ServeTrace {
level: trace_level,
events: measured.tracer.as_ref().map_or(&[], |t| t.events()),
layers: measured.layers.as_deref(),
wall_us: u64::try_from(duration.as_micros()).unwrap_or(u64::MAX),
prompt_tokens: prompt_token_count,
completion_tokens,
num_layers: measured.num_layers,
});
Some(build_chat_response_traced(
request_id.to_string(),
request.model.clone(),
response_text,
prompt_token_count,
completion_tokens,
budget,
request.stop.as_deref(),
traces,
request.tools.as_deref(),
request_tool_choice(request),
timings,
Some(reply.used_gpu),
logprobs,
))
}
struct TracedTurn {
tracer: Option<crate::inference_trace::InferenceTracer>,
layers: Option<Vec<crate::session::LayerTiming>>,
num_layers: usize,
}
const POISONED: &str =
"the Qwen3.5 session is unusable: an earlier request panicked while holding it (#3571)";
#[cfg(test)]
#[path = "qwen35_chat_backend_tests.rs"]
mod qwen35_serve_tests;