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:
2026-05-28 19:58:54 +08:00
parent 4c3f914459
commit d52baa0006
9 changed files with 1896 additions and 44 deletions

View File

@@ -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());
}
}