model: paged KV cache with CPU swap pool, decode graph, qwen3 updates
- paged_kv_cache: new block-paged KV cache; adds a pinned-host swap pool with
a second BlockAllocator, per-sequence Location {Gpu,Cpu}, and lossless
swap_out/swap_in (block-granular D2H/H2D) for vLLM-style preemption.
bytes_per_block helper exposes per-block cost for VRAM-based sizing.
- decode_graph: CUDA-graph decode path.
- qwen3/gpt2/kv_cache: paged prefill/decode forward + related updates.
- tokenizer/bins: BPE updates, new xserv-chat CLI, bench-qwen3 tweaks.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,7 @@ enum MergeEntry {
|
||||
struct AddedToken {
|
||||
id: u32,
|
||||
content: String,
|
||||
#[allow(dead_code)]
|
||||
special: bool,
|
||||
}
|
||||
|
||||
@@ -90,21 +91,22 @@ impl Tokenizer {
|
||||
}
|
||||
}
|
||||
|
||||
// Special tokens
|
||||
// Added tokens are matched as indivisible tokens by HF tokenizers,
|
||||
// even when their `special` flag is false (for example Qwen3's
|
||||
// <think> and </think> tokens).
|
||||
let mut special_tokens = HashMap::new();
|
||||
let mut special_token_ids = HashMap::new();
|
||||
let mut eos_token_id = None;
|
||||
for at in &tj.added_tokens {
|
||||
if at.special {
|
||||
special_tokens.insert(at.content.clone(), at.id);
|
||||
special_token_ids.insert(at.id, at.content.clone());
|
||||
decoder.resize(decoder.len().max(at.id as usize + 1), vec![]);
|
||||
decoder[at.id as usize] = at.content.as_bytes().to_vec();
|
||||
if at.content == "<|endoftext|>" || at.content == "<|end_of_text|>" {
|
||||
eos_token_id = Some(at.id);
|
||||
}
|
||||
}
|
||||
special_tokens.insert(at.content.clone(), at.id);
|
||||
special_token_ids.insert(at.id, at.content.clone());
|
||||
decoder.resize(decoder.len().max(at.id as usize + 1), vec![]);
|
||||
decoder[at.id as usize] = at.content.as_bytes().to_vec();
|
||||
}
|
||||
let eos_token_id = special_tokens
|
||||
.get("<|im_end|>")
|
||||
.or_else(|| special_tokens.get("<|end_of_text|>"))
|
||||
.or_else(|| special_tokens.get("<|endoftext|>"))
|
||||
.copied();
|
||||
|
||||
// Pre-tokenization regex
|
||||
let pre_tokenize_re = if byte_fallback {
|
||||
@@ -230,6 +232,19 @@ impl Tokenizer {
|
||||
String::from_utf8_lossy(&bytes).into_owned()
|
||||
}
|
||||
|
||||
pub fn decode_token_stream(&self, token_id: u32, pending: &mut Vec<u8>) -> String {
|
||||
if let Some(bytes) = self.decoder.get(token_id as usize) {
|
||||
pending.extend_from_slice(bytes);
|
||||
}
|
||||
take_valid_utf8(pending)
|
||||
}
|
||||
|
||||
pub fn flush_decode_stream(&self, pending: &mut Vec<u8>) -> String {
|
||||
let text = String::from_utf8_lossy(pending).into_owned();
|
||||
pending.clear();
|
||||
text
|
||||
}
|
||||
|
||||
pub fn eos_token_id(&self) -> Option<u32> {
|
||||
self.eos_token_id
|
||||
}
|
||||
@@ -250,6 +265,31 @@ fn token_str_to_bytes(s: &str) -> Vec<u8> {
|
||||
s.chars().map(|c| unicode_to_byte(c)).collect()
|
||||
}
|
||||
|
||||
fn take_valid_utf8(pending: &mut Vec<u8>) -> String {
|
||||
match std::str::from_utf8(pending) {
|
||||
Ok(text) => {
|
||||
let text = text.to_string();
|
||||
pending.clear();
|
||||
text
|
||||
}
|
||||
Err(err) => {
|
||||
let valid_up_to = err.valid_up_to();
|
||||
if valid_up_to == 0 {
|
||||
if let Some(error_len) = err.error_len() {
|
||||
let invalid_len = error_len.min(pending.len());
|
||||
let text = String::from_utf8_lossy(&pending[..invalid_len]).into_owned();
|
||||
pending.drain(..invalid_len);
|
||||
return text;
|
||||
}
|
||||
return String::new();
|
||||
}
|
||||
let text = String::from_utf8_lossy(&pending[..valid_up_to]).into_owned();
|
||||
pending.drain(..valid_up_to);
|
||||
text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a Unicode char back to the byte it represents in GPT-2 encoding.
|
||||
fn unicode_to_byte(c: char) -> u8 {
|
||||
// Build the inverse map on first use
|
||||
@@ -279,3 +319,49 @@ fn unicode_to_byte(c: char) -> u8 {
|
||||
panic!("unmapped unicode char U+{:04X} in tokenizer", c as u32)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{take_valid_utf8, Tokenizer};
|
||||
|
||||
#[test]
|
||||
fn qwen_added_tokens_are_indivisible_and_im_end_is_eos() {
|
||||
let path =
|
||||
std::env::temp_dir().join(format!("xserv-tokenizer-test-{}.json", std::process::id()));
|
||||
std::fs::write(
|
||||
&path,
|
||||
r#"{
|
||||
"model": {
|
||||
"vocab": {},
|
||||
"merges": [],
|
||||
"byte_fallback": false
|
||||
},
|
||||
"added_tokens": [
|
||||
{"id":151643,"content":"<|endoftext|>","special":true},
|
||||
{"id":151644,"content":"<|im_start|>","special":true},
|
||||
{"id":151645,"content":"<|im_end|>","special":true},
|
||||
{"id":151667,"content":"<think>","special":false},
|
||||
{"id":151668,"content":"</think>","special":false}
|
||||
]
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let tokenizer = Tokenizer::from_file(&path);
|
||||
let _ = std::fs::remove_file(&path);
|
||||
|
||||
assert_eq!(tokenizer.eos_token_id(), Some(151645));
|
||||
assert_eq!(tokenizer.encode("<think>"), vec![151667]);
|
||||
assert_eq!(tokenizer.encode("</think>"), vec![151668]);
|
||||
assert_eq!(tokenizer.decode(&[151645]), "<|im_end|>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stream_decode_buffers_incomplete_utf8() {
|
||||
let mut pending = vec![0xF0, 0x9F];
|
||||
assert_eq!(take_valid_utf8(&mut pending), "");
|
||||
pending.extend_from_slice(&[0x98, 0x8A, b'!']);
|
||||
assert_eq!(take_valid_utf8(&mut pending), "😊!");
|
||||
assert!(pending.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user