Files
xserv/crates/xserv-model/src/bin/xserv-cli.rs
Gahow Wang f5ec10c2c3 xserv-cli: expose sampling params and greedy repetition penalty
Interactive REPL used to always call sample_greedy_last on both the
paged and legacy KV paths, so temperature/top-k/top-p and the repetition
penalty added in the sampling module were unreachable from the CLI.

- flag() helper parses --max-tokens / --temperature / --top-k / --top-p
  / --rep-penalty / --rep-window (defaults preserve prior behavior:
  temperature 0, top-p 1, penalty 1, window 512).
- pick_next() dispatches to sample_greedy_penalized only when
  temperature==0 and rep_penalty>1, otherwise to sample().
- Both Qwen3/GPT-2 paths and the GptOss paged path share the same
  sampler and both feed the rolling history window used for the penalty.
- Prompt input now unescapes literal "\n" so multi-turn prompts can be
  typed on one line.
2026-07-01 14:16:31 +08:00

213 lines
7.1 KiB
Rust

use std::io::{self, Write};
use std::path::PathBuf;
use xserv_model::{
BLOCK_SIZE, KVCache, ModelConfig, PagedKVCache, SamplingParams, loader, sample,
sample_greedy_penalized,
};
use xserv_tensor::{DType, Device};
use xserv_tokenizer::Tokenizer;
fn flag<T: std::str::FromStr>(args: &[String], name: &str, default: T) -> T {
args.iter()
.position(|a| a == name)
.and_then(|i| args.get(i + 1))
.and_then(|s| s.parse().ok())
.unwrap_or(default)
}
fn pick_next(
logits: &xserv_tensor::Tensor,
sampling: &SamplingParams,
history: &[u32],
rep_penalty: f32,
) -> u32 {
if rep_penalty > 1.0 && sampling.temperature == 0.0 {
sample_greedy_penalized(logits, history, rep_penalty)
} else {
sample(logits, sampling)
}
}
fn main() {
let args: Vec<String> = std::env::args().collect();
if args.len() < 2 {
eprintln!(
"Usage: xserv-cli <model-dir> [--max-tokens N] [--temperature F] [--top-k N] [--top-p F] [--rep-penalty F] [--rep-window N]"
);
std::process::exit(1);
}
let model_dir = PathBuf::from(&args[1]);
let max_tokens = flag(&args, "--max-tokens", 100usize);
let sampling = SamplingParams {
temperature: flag(&args, "--temperature", 0.0f32),
top_k: flag(&args, "--top-k", 0usize),
top_p: flag(&args, "--top-p", 1.0f32),
};
let rep_penalty = flag(&args, "--rep-penalty", 1.0f32);
let rep_window = flag(&args, "--rep-window", 512usize);
xserv_cuda::device::set_device(0).unwrap();
let info = xserv_cuda::device::device_info(0).unwrap();
eprintln!(
"GPU: {} ({} MB free)",
info.name,
info.free_memory / 1024 / 1024
);
let config = ModelConfig::from_file(&model_dir.join("config.json"));
let model_type = config.model_type.as_deref().unwrap_or("unknown");
eprintln!(
"Model: {model_type}, layers={}, hidden={}, heads={}/{} kv, vocab={}",
config.num_layers(),
config.hidden(),
config.num_heads(),
config.num_kv_heads(),
config.vocab_size
);
eprintln!("Loading weights...");
let weights = loader::load_model_dir(&model_dir, Device::Cuda(0));
eprintln!("Loaded {} tensors", weights.len());
let is_qwen3 = model_type.contains("qwen");
let is_gpt_oss = model_type.contains("gpt_oss");
let dtype = if is_qwen3 || is_gpt_oss {
DType::BF16
} else {
DType::F32
};
// Build model
enum Model {
GPT2(xserv_model::GPT2),
Qwen3(xserv_model::Qwen3),
GptOss(xserv_model::GptOss),
}
let model = if is_gpt_oss {
Model::GptOss(xserv_model::GptOss::from_weights(config.clone(), weights))
} else if is_qwen3 {
Model::Qwen3(xserv_model::Qwen3::from_weights(config.clone(), weights))
} else {
Model::GPT2(xserv_model::GPT2::from_weights(config.clone(), weights))
};
let tokenizer = Tokenizer::from_file(&model_dir.join("tokenizer.json"));
eprintln!(
"Ready (KV cache, dtype={dtype}, temperature={}, top_k={}, top_p={}, rep_penalty={}, rep_window={}).\n",
sampling.temperature, sampling.top_k, sampling.top_p, rep_penalty, rep_window
);
loop {
print!("xserv> ");
io::stdout().flush().unwrap();
let mut input = String::new();
if io::stdin().read_line(&mut input).unwrap() == 0 {
break;
}
let raw_input = input.trim();
if raw_input.is_empty() {
continue;
}
if raw_input == "quit" || raw_input == "exit" {
break;
}
let input = raw_input.replace("\\n", "\n");
let token_ids = tokenizer.encode(&input);
if is_gpt_oss {
// GptOss uses paged KV cache
let max_seq = 2048;
let max_blocks_per_seq = (max_seq + BLOCK_SIZE - 1) / BLOCK_SIZE;
let total_blocks = max_blocks_per_seq + 64;
let mut paged_cache = PagedKVCache::new(
&config,
total_blocks,
0,
4,
max_blocks_per_seq,
DType::BF16,
0,
);
let slot = 0;
paged_cache.register_sequence(slot).expect("register slot");
let model = match &model {
Model::GptOss(m) => m,
_ => unreachable!(),
};
let logits = model.forward_prefill_paged(&token_ids, slot, &mut paged_cache);
let mut history = token_ids.clone();
let start = history.len().saturating_sub(rep_window);
let mut next = pick_next(&logits, &sampling, &history[start..], rep_penalty);
print!("{input}");
io::stdout().flush().unwrap();
for _ in 0..max_tokens {
let text = tokenizer.decode(&[next]);
print!("{text}");
io::stdout().flush().unwrap();
history.push(next);
if tokenizer.eos_token_id() == Some(next) {
break;
}
let pos = paged_cache.seq_len(slot);
let logits = model.forward_decode_paged(&[next], &[pos], &[slot], &mut paged_cache);
let start = history.len().saturating_sub(rep_window);
next = pick_next(&logits, &sampling, &history[start..], rep_penalty);
}
println!();
paged_cache.free_sequence(slot);
} else {
let kv_heads = if is_qwen3 {
config.num_kv_heads()
} else {
config.num_heads()
};
let mut cache = KVCache::new(
config.num_layers(),
kv_heads,
config.head_dim(),
dtype,
Device::Cuda(0),
);
let logits = match &model {
Model::GPT2(m) => m.forward_with_cache(&token_ids, &mut cache),
Model::Qwen3(m) => m.forward_with_cache(&token_ids, &mut cache),
Model::GptOss(_) => unreachable!(),
};
let mut history = token_ids.clone();
let start = history.len().saturating_sub(rep_window);
let mut next = pick_next(&logits, &sampling, &history[start..], rep_penalty);
print!("{input}");
io::stdout().flush().unwrap();
for _ in 0..max_tokens {
let text = tokenizer.decode(&[next]);
print!("{text}");
io::stdout().flush().unwrap();
history.push(next);
if tokenizer.eos_token_id() == Some(next) {
break;
}
let logits = match &model {
Model::GPT2(m) => m.forward_with_cache(&[next], &mut cache),
Model::Qwen3(m) => m.forward_with_cache(&[next], &mut cache),
Model::GptOss(_) => unreachable!(),
};
let start = history.len().saturating_sub(rep_window);
next = pick_next(&logits, &sampling, &history[start..], rep_penalty);
}
println!();
}
}
}