Integrate Qwen3.8 model intake
This commit is contained in:
1 parent
4c83ac0360
commit
3773cfda2e
15 files changed
+1805
-35
No files matched your search
+275
-12
@@ -4,7 +4,10 @@ use super::{
|
||||
VISION_TOKEN_END, VISION_TOKEN_START,
|
||||
};
|
||||
use crate::settings::ReasoningMode;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
const HIGH_REASONING_PREFIX: &str = "Reasoning Effort: Absolute maximum with no shortcuts permitted.\n\
|
||||
You MUST be very thorough in your thinking and comprehensively decompose the problem to resolve the root cause, rigorously stress-testing your logic against all potential paths, edge cases, and adversarial scenarios.\n\
|
||||
@@ -20,7 +23,14 @@ fn reasoning_prefix(family: ModelFamily, reasoning: ReasoningMode) -> Option<&'s
|
||||
(ModelFamily::Glm, ReasoningMode::Low) => Some("Reasoning Effort: Low"),
|
||||
(ModelFamily::Glm, ReasoningMode::High) => Some("Reasoning Effort: High"),
|
||||
(ModelFamily::Glm, ReasoningMode::Max) => Some("Reasoning Effort: Max"),
|
||||
(_, ReasoningMode::Direct | ReasoningMode::Low) => None,
|
||||
(ModelFamily::Qwen, _) => None,
|
||||
(
|
||||
_,
|
||||
ReasoningMode::Direct
|
||||
| ReasoningMode::Low
|
||||
| ReasoningMode::Medium
|
||||
| ReasoningMode::XHigh,
|
||||
) => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +48,30 @@ pub(super) struct Tokenizer {
|
||||
sop: i32,
|
||||
think_start: i32,
|
||||
think_end: i32,
|
||||
rendered_specials: Vec<(&'static [u8], i32)>,
|
||||
alternate_eos: i32,
|
||||
rendered_specials: Vec<(Vec<u8>, i32)>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct QwenTokenizerFile {
|
||||
added_tokens: Vec<QwenAddedToken>,
|
||||
model: QwenBpe,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct QwenAddedToken {
|
||||
id: usize,
|
||||
content: String,
|
||||
special: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct QwenBpe {
|
||||
#[serde(rename = "type")]
|
||||
kind: String,
|
||||
vocab: HashMap<String, usize>,
|
||||
merges: Vec<String>,
|
||||
byte_fallback: bool,
|
||||
}
|
||||
|
||||
impl Tokenizer {
|
||||
@@ -95,6 +128,9 @@ impl Tokenizer {
|
||||
lookup(b"<think>"),
|
||||
lookup(b"</think>"),
|
||||
),
|
||||
ModelFamily::Qwen => {
|
||||
return Err("Qwen tokenizers must be loaded from tokenizer.json".into());
|
||||
}
|
||||
};
|
||||
if [bos, user, assistant, think_start, think_end]
|
||||
.into_iter()
|
||||
@@ -128,6 +164,7 @@ impl Tokenizer {
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|(_, token)| *token >= 0)
|
||||
.map(|(marker, token)| (marker.to_vec(), token))
|
||||
.collect();
|
||||
|
||||
Ok(Self {
|
||||
@@ -144,6 +181,98 @@ impl Tokenizer {
|
||||
sop,
|
||||
think_start,
|
||||
think_end,
|
||||
alternate_eos: -1,
|
||||
rendered_specials,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn load_qwen(path: &Path) -> Result<Self, String> {
|
||||
let file: QwenTokenizerFile = serde_json::from_slice(
|
||||
&fs::read(path).map_err(|error| format!("{}: {error}", path.display()))?,
|
||||
)
|
||||
.map_err(|error| format!("{}: {error}", path.display()))?;
|
||||
if file.model.kind != "BPE" || file.model.byte_fallback {
|
||||
return Err("Qwen tokenizer must use BPE without byte fallback".into());
|
||||
}
|
||||
let maximum = file
|
||||
.model
|
||||
.vocab
|
||||
.values()
|
||||
.copied()
|
||||
.chain(file.added_tokens.iter().map(|token| token.id))
|
||||
.max()
|
||||
.ok_or_else(|| "Qwen tokenizer vocabulary is empty".to_owned())?;
|
||||
if maximum != 248_076 {
|
||||
return Err(format!(
|
||||
"Qwen tokenizer ends at token id {maximum}, expected 248076"
|
||||
));
|
||||
}
|
||||
// The model head has 248320 rows; the final 243 ids are deliberately
|
||||
// reserved and have no tokenizer spelling in the pinned source.
|
||||
let mut tokens = vec![Vec::new(); 248_320];
|
||||
for (token, id) in file.model.vocab {
|
||||
if !tokens[id].is_empty() {
|
||||
return Err(format!("Qwen tokenizer has duplicate token id {id}"));
|
||||
}
|
||||
tokens[id] = token.into_bytes();
|
||||
}
|
||||
let mut rendered_specials = Vec::new();
|
||||
for token in file.added_tokens {
|
||||
if !tokens[token.id].is_empty() {
|
||||
return Err(format!(
|
||||
"Qwen tokenizer has duplicate token id {}",
|
||||
token.id
|
||||
));
|
||||
}
|
||||
tokens[token.id] = token.content.as_bytes().to_vec();
|
||||
if token.special {
|
||||
rendered_specials.push((token.content.into_bytes(), token.id as i32));
|
||||
}
|
||||
}
|
||||
if tokens[..=maximum].iter().any(Vec::is_empty) {
|
||||
return Err("Qwen tokenizer token ids are not contiguous".into());
|
||||
}
|
||||
let token_to_id = tokens
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, token)| !token.is_empty())
|
||||
.map(|(id, token)| (token.clone(), id as i32))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let required = |token: &[u8]| {
|
||||
token_to_id.get(token).copied().ok_or_else(|| {
|
||||
format!(
|
||||
"required Qwen tokenizer token is missing: {}",
|
||||
String::from_utf8_lossy(token)
|
||||
)
|
||||
})
|
||||
};
|
||||
let merge_rank = file
|
||||
.model
|
||||
.merges
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(rank, merge)| (merge.into_bytes(), rank))
|
||||
.collect();
|
||||
let alternate_eos = required(b"<|endoftext|>")?;
|
||||
let im_start = required(b"<|im_start|>")?;
|
||||
let im_end = required(b"<|im_end|>")?;
|
||||
let think_start = required(b"<think>")?;
|
||||
let think_end = required(b"</think>")?;
|
||||
Ok(Self {
|
||||
family: ModelFamily::Qwen,
|
||||
tokens,
|
||||
token_to_id,
|
||||
merge_rank,
|
||||
bos: alternate_eos,
|
||||
eos: im_end,
|
||||
system: im_start,
|
||||
user: im_start,
|
||||
assistant: im_start,
|
||||
observation: im_start,
|
||||
sop: im_end,
|
||||
think_start,
|
||||
think_end,
|
||||
alternate_eos,
|
||||
rendered_specials,
|
||||
})
|
||||
}
|
||||
@@ -152,6 +281,40 @@ impl Tokenizer {
|
||||
self.tokens.len()
|
||||
}
|
||||
|
||||
pub(super) fn validate_qwen_contract(&self) -> Result<(), String> {
|
||||
if self.family != ModelFamily::Qwen {
|
||||
return Err("tokenizer is not Qwen".into());
|
||||
}
|
||||
for (token, expected) in [
|
||||
(b"<|endoftext|>".as_slice(), 248_044),
|
||||
(b"<|im_start|>".as_slice(), 248_045),
|
||||
(b"<|im_end|>".as_slice(), 248_046),
|
||||
(b"<tool_call>".as_slice(), 248_058),
|
||||
(b"</tool_call>".as_slice(), 248_059),
|
||||
(b"<tool_response>".as_slice(), 248_066),
|
||||
(b"</tool_response>".as_slice(), 248_067),
|
||||
(b"<think>".as_slice(), 248_068),
|
||||
(b"</think>".as_slice(), 248_069),
|
||||
] {
|
||||
if self.token_to_id.get(token).copied() != Some(expected) {
|
||||
return Err(format!(
|
||||
"Qwen special token {} does not have id {expected}",
|
||||
String::from_utf8_lossy(token)
|
||||
));
|
||||
}
|
||||
}
|
||||
if ![248_044, 248_045, 248_046]
|
||||
.into_iter()
|
||||
.all(|token| self.is_ple_reset(token))
|
||||
|| ![248_044, 248_046]
|
||||
.into_iter()
|
||||
.all(|token| self.is_stop(token))
|
||||
{
|
||||
return Err("Qwen EOS or PLE reset markers are invalid".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn token_bytes(&self, token: i32) -> Option<Vec<u8>> {
|
||||
let token = self.tokens.get(usize::try_from(token).ok()?)?;
|
||||
if token.windows(3).any(|window| window == [0xef, 0xbd, 0x9c]) {
|
||||
@@ -163,10 +326,10 @@ impl Tokenizer {
|
||||
|
||||
pub(super) fn tokenize(&self, text: &str) -> Vec<i32> {
|
||||
let mut output = Vec::new();
|
||||
if self.family == ModelFamily::Glm {
|
||||
self.tokenize_glm(text, &mut output);
|
||||
} else {
|
||||
self.tokenize_joyai(text, &mut output);
|
||||
match self.family {
|
||||
ModelFamily::DeepSeek => self.tokenize_joyai(text, &mut output),
|
||||
ModelFamily::Glm => self.tokenize_gpt4(text, &mut output, 3),
|
||||
ModelFamily::Qwen => self.tokenize_gpt4(text, &mut output, 1),
|
||||
}
|
||||
output
|
||||
}
|
||||
@@ -211,10 +374,10 @@ impl Tokenizer {
|
||||
}
|
||||
|
||||
fn tokenize_plain(&self, text: &str, output: &mut Vec<i32>) {
|
||||
if self.family == ModelFamily::Glm {
|
||||
self.tokenize_glm(text, output);
|
||||
} else {
|
||||
self.tokenize_joyai(text, output);
|
||||
match self.family {
|
||||
ModelFamily::DeepSeek => self.tokenize_joyai(text, output),
|
||||
ModelFamily::Glm => self.tokenize_gpt4(text, output, 3),
|
||||
ModelFamily::Qwen => self.tokenize_gpt4(text, output, 1),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,6 +427,14 @@ impl Tokenizer {
|
||||
reasoning: ReasoningMode,
|
||||
continue_assistant: bool,
|
||||
) -> Vec<i32> {
|
||||
if self.family == ModelFamily::Qwen {
|
||||
return self.encode_qwen_messages(
|
||||
system_prompt,
|
||||
messages,
|
||||
reasoning,
|
||||
continue_assistant,
|
||||
);
|
||||
}
|
||||
let mut output = vec![self.bos];
|
||||
if self.family == ModelFamily::Glm && self.sop >= 0 {
|
||||
output.push(self.sop);
|
||||
@@ -353,12 +524,95 @@ impl Tokenizer {
|
||||
output
|
||||
}
|
||||
|
||||
fn encode_qwen_messages(
|
||||
&self,
|
||||
system_prompt: &str,
|
||||
messages: &[ChatTurn],
|
||||
reasoning: ReasoningMode,
|
||||
continue_assistant: bool,
|
||||
) -> Vec<i32> {
|
||||
let instruction = match reasoning {
|
||||
ReasoningMode::Direct | ReasoningMode::Medium => "",
|
||||
ReasoningMode::Low => {
|
||||
"Reasoning effort is set to low. Keep your thinking brief and focused, moving directly to the conclusion without unnecessary elaboration."
|
||||
}
|
||||
ReasoningMode::XHigh => {
|
||||
"Reasoning effort is set to xhigh. Please think carefully through the task, validate key assumptions, consider plausible alternatives, and prioritize correctness, consistency, and clarity in the final answer."
|
||||
}
|
||||
ReasoningMode::High | ReasoningMode::Max => {
|
||||
unreachable!("legacy reasoning modes are not valid for Qwen")
|
||||
}
|
||||
};
|
||||
let mut rendered = String::new();
|
||||
let system_prompt = system_prompt.trim();
|
||||
if !instruction.is_empty() || !system_prompt.is_empty() {
|
||||
rendered.push_str("<|im_start|>system\n");
|
||||
if !instruction.is_empty() {
|
||||
rendered.push_str(instruction);
|
||||
if !system_prompt.is_empty() {
|
||||
rendered.push_str("\n\n");
|
||||
}
|
||||
}
|
||||
rendered.push_str(system_prompt);
|
||||
rendered.push_str("<|im_end|>\n");
|
||||
}
|
||||
for (index, message) in messages.iter().enumerate() {
|
||||
let content = message.content.trim();
|
||||
if message.system {
|
||||
rendered.push_str("<|im_start|>system\n");
|
||||
rendered.push_str(content);
|
||||
rendered.push_str("<|im_end|>\n");
|
||||
} else if message.tool {
|
||||
if index == 0 || !messages[index - 1].tool {
|
||||
rendered.push_str("<|im_start|>user");
|
||||
}
|
||||
rendered.push_str("\n<tool_response>\n");
|
||||
rendered.push_str(content);
|
||||
rendered.push_str("\n</tool_response>");
|
||||
if index + 1 == messages.len() || !messages[index + 1].tool {
|
||||
rendered.push_str("<|im_end|>\n");
|
||||
}
|
||||
} else if message.user {
|
||||
rendered.push_str("<|im_start|>user\n");
|
||||
rendered.push_str(content);
|
||||
rendered.push_str("<|im_end|>\n");
|
||||
} else {
|
||||
rendered.push_str("<|im_start|>assistant\n<think>\n");
|
||||
if let Some(thinking) = &message.reasoning {
|
||||
rendered.push_str(thinking.trim());
|
||||
}
|
||||
rendered.push_str("\n</think>\n\n");
|
||||
rendered.push_str(content);
|
||||
rendered.push_str("<|im_end|>\n");
|
||||
}
|
||||
}
|
||||
if continue_assistant {
|
||||
rendered.push_str("<|im_start|>assistant\n<think>\n");
|
||||
if reasoning == ReasoningMode::Direct {
|
||||
rendered.push_str("\n</think>\n\n");
|
||||
}
|
||||
}
|
||||
self.tokenize_rendered(&rendered)
|
||||
}
|
||||
|
||||
pub(super) fn encode_continuation(
|
||||
&self,
|
||||
prompt: &str,
|
||||
reasoning: ReasoningMode,
|
||||
skip_previous_eos: bool,
|
||||
) -> Vec<i32> {
|
||||
if self.family == ModelFamily::Qwen {
|
||||
let messages = [ChatTurn {
|
||||
user: true,
|
||||
tool: false,
|
||||
system: false,
|
||||
skip_previous_eos,
|
||||
reasoning: None,
|
||||
reasoning_complete: true,
|
||||
content: prompt.to_owned(),
|
||||
}];
|
||||
return self.encode_qwen_messages("", &messages, reasoning, true);
|
||||
}
|
||||
let mut output = Vec::new();
|
||||
if self.family == ModelFamily::DeepSeek && !skip_previous_eos {
|
||||
output.push(self.eos);
|
||||
@@ -382,6 +636,7 @@ impl Tokenizer {
|
||||
|
||||
pub(super) fn is_stop(&self, token: i32) -> bool {
|
||||
token == self.eos
|
||||
|| token == self.alternate_eos
|
||||
|| (self.family == ModelFamily::Glm
|
||||
&& [self.system, self.user, self.assistant, self.observation].contains(&token))
|
||||
}
|
||||
@@ -394,6 +649,11 @@ impl Tokenizer {
|
||||
token == self.think_end
|
||||
}
|
||||
|
||||
pub(super) fn is_ple_reset(&self, token: i32) -> bool {
|
||||
self.family == ModelFamily::Qwen
|
||||
&& [self.system, self.sop, self.alternate_eos].contains(&token)
|
||||
}
|
||||
|
||||
fn emit_piece(&self, raw: &[u8], output: &mut Vec<i32>) {
|
||||
if raw.is_empty() {
|
||||
return;
|
||||
@@ -512,7 +772,7 @@ impl Tokenizer {
|
||||
}
|
||||
}
|
||||
|
||||
fn tokenize_glm(&self, text: &str, output: &mut Vec<i32>) {
|
||||
fn tokenize_gpt4(&self, text: &str, output: &mut Vec<i32>, max_digits: usize) {
|
||||
let mut position = 0;
|
||||
while position < text.len() {
|
||||
let start = position;
|
||||
@@ -548,7 +808,10 @@ impl Tokenizer {
|
||||
}
|
||||
} else if current.number {
|
||||
let mut digits = 0;
|
||||
while position < text.len() && char_info(text, position).number && digits < 3 {
|
||||
while position < text.len()
|
||||
&& char_info(text, position).number
|
||||
&& digits < max_digits
|
||||
{
|
||||
position = char_info(text, position).next;
|
||||
digits += 1;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user