pub struct Tokenizer {
pub bos_token_id: Option<u32>,
pub eos_token_id: Option<u32>,
pub pad_token_id: Option<u32>,
pub im_start_id: Option<u32>,
pub im_end_id: Option<u32>,
pub chat_template: Option<String>,
pub extra_eos: HashSet<u32>,
pub add_bos: bool,
/* private fields */
}Expand description
A loaded BPE tokenizer.
Fields§
§bos_token_id: Option<u32>Special tokens
eos_token_id: Option<u32>§pad_token_id: Option<u32>§im_start_id: Option<u32>Chat template special tokens
im_end_id: Option<u32>§chat_template: Option<String>Jinja chat template carried by the container (spec §6.1); None → hardcoded ChatML fallback.
extra_eos: HashSet<u32>Extra stop ids from the container’s generation config.
add_bos: boolGeneration prepends BOS (llama post_processor semantics).
Implementations§
Source§impl Tokenizer
impl Tokenizer
Sourcepub fn from_file(path: impl AsRef<Path>) -> Result<Self, TokenizerError>
pub fn from_file(path: impl AsRef<Path>) -> Result<Self, TokenizerError>
Load tokenizer from HuggingFace tokenizer.json file.
Sourcepub fn from_bytes(bytes: &[u8]) -> Result<Self, TokenizerError>
pub fn from_bytes(bytes: &[u8]) -> Result<Self, TokenizerError>
Load tokenizer from raw tokenizer.json bytes (CMF VOCAB section).
Examples found in repository?
32fn main() {
33 let a: Vec<String> = std::env::args().collect();
34 let model = Arc::new(cortiq_core::CmfModel::open(&a[1]).unwrap());
35 let (prompt, hh, ww) = (&a[2], a[3].parse::<usize>().unwrap(), a[4].parse::<usize>().unwrap());
36 let (steps, shift, i) = (a[5].parse::<usize>().unwrap(), a[6].parse::<f32>().unwrap(), a[7].parse::<usize>().unwrap());
37 let tok = Tokenizer::from_bytes(model.vocab.as_deref().unwrap()).unwrap();
38 let ids = cortiq_engine::zimagegen::prompt_ids(&tok, prompt, 512);
39 let cap = {
40 let _p = cortiq_engine::gpu::pause_gpu();
41 cortiq_engine::qwen3te::Qwen3Encoder::from_cmf(&model).unwrap().encode(&ids)
42 };
43 let dit = ZImageDit::from_cmf(&model).unwrap();
44 let sig = zimage::sigmas_torch_f32(steps, shift);
45 let t = zimage::t_model(sig[i]);
46 let mods = dit.mods_for_steps(&[t]);
47 let fs = dit.final_scale_for_steps(&[t]);
48 let shape = ZShape::new(hh, ww, ids.len());
49 let prep = dit.prepare(&cap, shape, 1, None).unwrap();
50 println!("device prepared: {}", prep.device);
51 let (c, lh, lw) = (dit.cfg.in_channels, hh / 8, ww / 8);
52 let lat = |dir: &str| -> Vec<f32> {
53 if i == 0 {
54 read_f32(&std::env::var("CMF_INIT_LATENT").unwrap())
55 } else {
56 read_f32(&format!("{dir}/lat_{i}.f32"))
57 }
58 };
59 let xa = lat(&a[8]);
60 // `ZC_NEG=<negative prompt>`: the CFG pair as one batch-2 device
61 // forward against the two items stepped one by one.
62 if let Ok(neg) = std::env::var("ZC_NEG") {
63 let nids = cortiq_engine::zimagegen::prompt_ids(&tok, &neg, 512);
64 let ncap = {
65 let _p = cortiq_engine::gpu::pause_gpu();
66 cortiq_engine::qwen3te::Qwen3Encoder::from_cmf(&model).unwrap().encode(&nids)
67 };
68 let mut np = dit.prepare(&ncap, ZShape::new(hh, ww, nids.len()), 2, None).unwrap();
69 let tok_a = dit.tokens(&xa, &shape);
70 // `ZC_TAPS=<dir>`: every block's residual stream of the three
71 // forwards (single pos, single neg, pair) into <dir>/{pos,neg,pair},
72 // then compared block by block (the pair's item rows vs the single).
73 let taps = std::env::var("ZC_TAPS").ok();
74 let set_taps = |sub: &str| {
75 if let Some(d) = &taps {
76 unsafe { std::env::set_var("CMF_ZI_TAPS", format!("{d}/{sub}")) };
77 }
78 };
79 set_taps("pos");
80 let vp = dit.step(&prep, i, &tok_a, &mods, &fs);
81 set_taps("neg");
82 let vn = dit.step(&np, i, &tok_a, &mods, &fs);
83 unsafe { std::env::remove_var("CMF_ZI_TAPS") };
84 np.device = false;
85 let vn_cpu = dit.step(&np, i, &tok_a, &mods, &fs);
86 np.device = true;
87 // `ZC_SWAP=1`: the pair as (neg, pos) — tells a positional cause
88 // (item 0 vs item 1) from a content one (the caption length).
89 let swap = std::env::var("ZC_SWAP").as_deref() == Ok("1");
90 let (first, second) = if swap { (&np, &prep) } else { (&prep, &np) };
91 let ok = dit.attach_device_pair(first, second, 3, None);
92 println!(
93 "pair prepared: {ok} (pos L = {}, neg L = {}, n_img_p {}, order {})",
94 ids.len(),
95 nids.len(),
96 shape.n_img_p,
97 if swap { "neg,pos" } else { "pos,neg" }
98 );
99 set_taps("pair");
100 if let Some((p0, p1)) = dit.step_pair_device(3, shape.n_img, i, &tok_a, &mods, &fs) {
101 let (pp, pn) = if swap { (p1, p0) } else { (p0, p1) };
102 let nan = pp.iter().chain(&pn).filter(|v| !v.is_finite()).count();
103 println!("pair vs singles: pos {:.3e} neg {:.3e} non-finite {nan} single neg dev vs cpu {:.3e}",
104 rel(&pp, &vp), rel(&pn, &vn), rel(&vn, &vn_cpu));
105 }
106 unsafe { std::env::remove_var("CMF_ZI_TAPS") };
107 if let Some(d) = &taps {
108 // Row layout of the pair: image stage [item][n_img_p]; joint
109 // stage [item0: n_img_p + cp0][item1: n_img_p + cp1].
110 let h = dit.cfg.dim;
111 let cp = |l: usize| l.div_ceil(32) * 32;
112 let (cp_pos, cp_neg) = (cp(ids.len()), cp(nids.len()));
113 let (cp0, cp1) = if swap { (cp_neg, cp_pos) } else { (cp_pos, cp_neg) };
114 let nip = shape.n_img_p;
115 let rd = |p: String| -> Option<Vec<f32>> {
116 std::fs::read(&p).ok().map(|b| b.chunks_exact(4).map(|c| f32::from_le_bytes(c.try_into().unwrap())).collect())
117 };
118 let mut names: Vec<String> = (0..2).map(|k| format!("nr{k}_out")).collect();
119 names.extend((0..dit.cfg.n_layers).map(|k| format!("l{k}_out")));
120 for n in names {
121 let (Some(sp), Some(sn), Some(pr)) = (
122 rd(format!("{d}/pos/step{i}/{n}.f32")),
123 rd(format!("{d}/neg/step{i}/{n}.f32")),
124 rd(format!("{d}/pair/step{i}/{n}.f32")),
125 ) else {
126 continue;
127 };
128 let (r0, r1) = if n.starts_with("nr") {
129 ((0, nip), (nip, nip))
130 } else {
131 ((0, nip + cp0), (nip + cp0, nip + cp1))
132 };
133 let item = |r: (usize, usize)| &pr[r.0 * h..(r.0 + r.1) * h];
134 let (ip, ineg) = if swap { (item(r1), item(r0)) } else { (item(r0), item(r1)) };
135 // image rows and caption rows separately
136 let split = |v: &[f32]| (v[..nip * h].to_vec(), v[nip * h..].to_vec());
137 let (pi, pc) = split(ip);
138 let (spi, spc) = split(&sp[..ip.len().min(sp.len())]);
139 println!(
140 "{n:9} pos: img {:.3e} cap {:.3e} neg: all {:.3e}",
141 rel(&pi, &spi),
142 if pc.is_empty() { 0.0 } else { rel(&pc, &spc) },
143 rel(ineg, &sn[..ineg.len().min(sn.len())])
144 );
145 }
146 }
147 return;
148 }
149 let tok_a = dit.tokens(&xa, &shape);
150 let va_dev = zimage::unpatchify(&dit.step(&prep, i, &tok_a, &mods, &fs), c, lh, lw);
151 let mut hp = zimage::ZPrepared { device: false, ..prep };
152 let va_cpu = zimage::unpatchify(&dit.step(&hp, i, &tok_a, &mods, &fs), c, lh, lw);
153 let va_tr = read_f32(&format!("{}/v_{i}.f32", a[8]));
154 println!("step {i} on A's lat: dev vs cpu {:.3e} cpu vs A's v {:.3e} dev vs A's v {:.3e}",
155 rel(&va_dev, &va_cpu), rel(&va_cpu, &va_tr), rel(&va_dev, &va_tr));
156 if let Some(b) = a.get(9) {
157 hp.device = true;
158 let xb = lat(b);
159 let vb_dev = zimage::unpatchify(&dit.step(&hp, i, &dit.tokens(&xb, &shape), &mods, &fs), c, lh, lw);
160 println!("inputs A vs B {:.3e} device outputs {:.3e} (B's own v {:.3e})",
161 rel(&xb, &xa), rel(&vb_dev, &va_dev), rel(&read_f32(&format!("{b}/v_{i}.f32")), &va_dev));
162 }
163}Sourcepub fn from_json(json: &str) -> Result<Self, TokenizerError>
pub fn from_json(json: &str) -> Result<Self, TokenizerError>
Load tokenizer from JSON string.
Sourcepub fn byte_level() -> Self
pub fn byte_level() -> Self
Create a minimal tokenizer for testing (byte tokens, no merges).
Sourcepub fn encode(&self, text: &str) -> Vec<u32>
pub fn encode(&self, text: &str) -> Vec<u32>
Encode text to token IDs.
Examples found in repository?
17fn main() {
18 let args: Vec<String> = std::env::args().collect();
19 if args.len() < 3 {
20 eprintln!("usage: topk_probe <model.cmf> <raw text> [k]");
21 std::process::exit(2);
22 }
23 let k: usize = args.get(3).and_then(|s| s.parse().ok()).unwrap_or(10);
24
25 let model = Arc::new(CmfModel::open_sharded(&args[1]).expect("open model"));
26 let mut pipeline = Pipeline::from_model(&model, SamplerConfig::default()).expect("pipeline");
27 let ids = pipeline.tokenizer.encode(&args[2]);
28 eprintln!("prompt tokens: {}", ids.len());
29 eprintln!("last 8 ids: {:?}", &ids[ids.len().saturating_sub(8)..]);
30
31 let logits = pipeline.prefill_next_logits(&ids, None);
32 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
33 let sum: f64 = logits.iter().map(|&l| ((l - max) as f64).exp()).sum();
34
35 let mut order: Vec<usize> = (0..logits.len()).collect();
36 order.sort_by(|&a, &b| logits[b].total_cmp(&logits[a]));
37 for &i in order.iter().take(k) {
38 let p = ((logits[i] - max) as f64).exp() / sum;
39 println!(
40 "{:>8} logit {:>9.4} p {:.6} {:?}",
41 i,
42 logits[i],
43 p,
44 pipeline.tokenizer.decode_token(i as u32)
45 );
46 }
47}More examples
18fn main() {
19 let args: Vec<String> = std::env::args().collect();
20 if args.len() < 4 {
21 eprintln!("usage: decode_flat <model.cmf> <ctx> <tokens> [o1spec|off]");
22 std::process::exit(2);
23 }
24 let path = &args[1];
25 let ctx: usize = args[2].parse().expect("ctx");
26 let tokens: usize = args[3].parse().expect("tokens");
27 let o1spec = args.get(4).cloned().unwrap_or_else(|| "off".to_string());
28
29 let model = Arc::new(CmfModel::open_sharded(path).expect("open model"));
30 let mut pipeline = Pipeline::from_model(&model, SamplerConfig::default()).expect("pipeline");
31 pipeline.set_o1(cortiq_engine::nystrom::O1Cfg::from_spec(
32 &o1spec, None, None, None, None,
33 ));
34 eprintln!("o1_active = {}", pipeline.o1_active());
35
36 // Same synthetic prompt shape as `cortiq bench --ctx`.
37 let prompt = "The quick brown fox jumps over the lazy dog. ".repeat(ctx / 8 + 2);
38 let mut ids = pipeline.tokenizer.encode(&prompt);
39 ids.truncate(ctx);
40 assert_eq!(ids.len(), ctx, "prompt too short for ctx");
41
42 // Warm the mmap so the first token is not billed for page faults.
43 let _ = pipeline.forward_ids(&ids[..2], None).expect("warm");
44
45 let stamps: Arc<std::sync::Mutex<Vec<Instant>>> = Arc::default();
46 let st = stamps.clone();
47 let cb: cortiq_engine::TokenCallback = Box::new(move |_t| {
48 st.lock().unwrap().push(Instant::now());
49 true
50 });
51 let t0 = Instant::now();
52 let _ = pipeline
53 .generate_from_ids(&ids, tokens, None, Some(cb))
54 .expect("generate");
55
56 let stamps = stamps.lock().unwrap();
57 // stamps[0] fires after generation's prefill + the one-off o1 seal:
58 // that gap is TTFT, not a decode step.
59 println!(
60 "ctx={ctx} o1={o1spec} ttft_s={:.3}",
61 stamps[0].duration_since(t0).as_secs_f64()
62 );
63 println!("# idx ms");
64 for i in 1..stamps.len() {
65 println!(
66 "{:4} {:7.2}",
67 i,
68 (stamps[i] - stamps[i - 1]).as_secs_f64() * 1e3
69 );
70 }
71 // Mean of the last half — past any startup transient.
72 let half = stamps.len() / 2;
73 if stamps.len() > 2 && half >= 1 {
74 let dt = (stamps[stamps.len() - 1] - stamps[half]).as_secs_f64();
75 let n = (stamps.len() - 1 - half) as f64;
76 println!(
77 "last-half: {:.2} ms/tok = {:.2} tok/s",
78 dt / n * 1e3,
79 n / dt
80 );
81 }
82}Sourcepub fn decode(&self, ids: &[u32]) -> String
pub fn decode(&self, ids: &[u32]) -> String
Decode token IDs back to text. Special tokens are skipped; added tokens are raw text; everything else reverses the byte-level map.
Sourcepub fn decode_token(&self, id: u32) -> String
pub fn decode_token(&self, id: u32) -> String
Streaming decode of ONE token: no sequence-level Strip — a per-token strip would eat the ▁-spaces of every SP word.
Examples found in repository?
17fn main() {
18 let args: Vec<String> = std::env::args().collect();
19 if args.len() < 3 {
20 eprintln!("usage: topk_probe <model.cmf> <raw text> [k]");
21 std::process::exit(2);
22 }
23 let k: usize = args.get(3).and_then(|s| s.parse().ok()).unwrap_or(10);
24
25 let model = Arc::new(CmfModel::open_sharded(&args[1]).expect("open model"));
26 let mut pipeline = Pipeline::from_model(&model, SamplerConfig::default()).expect("pipeline");
27 let ids = pipeline.tokenizer.encode(&args[2]);
28 eprintln!("prompt tokens: {}", ids.len());
29 eprintln!("last 8 ids: {:?}", &ids[ids.len().saturating_sub(8)..]);
30
31 let logits = pipeline.prefill_next_logits(&ids, None);
32 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
33 let sum: f64 = logits.iter().map(|&l| ((l - max) as f64).exp()).sum();
34
35 let mut order: Vec<usize> = (0..logits.len()).collect();
36 order.sort_by(|&a, &b| logits[b].total_cmp(&logits[a]));
37 for &i in order.iter().take(k) {
38 let p = ((logits[i] - max) as f64).exp() / sum;
39 println!(
40 "{:>8} logit {:>9.4} p {:.6} {:?}",
41 i,
42 logits[i],
43 p,
44 pipeline.tokenizer.decode_token(i as u32)
45 );
46 }
47}Sourcepub fn decode_token_for_hash(&self, id: u32) -> String
pub fn decode_token_for_hash(&self, id: u32) -> String
Decode one vocabulary entry for Engram’s compressed token map while retaining special tokens.
Sourcepub fn decode_for_protocol(&self, ids: &[u32]) -> String
pub fn decode_for_protocol(&self, ids: &[u32]) -> String
Decode generated protocol text while retaining special markers. The
V4.1 harmony parser needs <think>, EOS, and spaced DSML tags.
Sourcepub fn raw_token_for_hash(&self, id: u32) -> String
pub fn raw_token_for_hash(&self, id: u32) -> String
Return the backend vocabulary spelling for an Engram map entry.
Sourcepub fn apply_chat_template(&self, messages: &[(String, String)]) -> Vec<u32>
pub fn apply_chat_template(&self, messages: &[(String, String)]) -> Vec<u32>
Render the container’s Jinja chat template (HF semantics: trim_blocks + lstrip_blocks + loop controls) and encode it. Falls back to hardcoded ChatML when the file carries none.
Sourcepub fn apply_chat_template_json(
&self,
messages: &[Value],
tools: Option<&[Value]>,
enable_thinking: Option<bool>,
) -> Vec<u32>
pub fn apply_chat_template_json( &self, messages: &[Value], tools: Option<&[Value]>, enable_thinking: Option<bool>, ) -> Vec<u32>
Like apply_chat_template, with an explicit enable_thinking value for
reasoning-model templates (Qwen3/3.5 emit an empty None leaves the variable
undefined — the template’s own default applies.
Chat template with the FULL message shape and a tool list.
The pair-based API below flattens every message to (role, text),
which silently drops exactly what agentic use needs: the tools
array, role: "tool" results, and tool_calls on assistant
turns. The templates this format embeds — Qwen-family, Nanbeige —
have carried a {%- if tools %} branch all along; this is the
call that finally feeds it. Messages arrive as JSON objects in
the OpenAI shape and pass through to minijinja unflattened, so a
template sees the same fields a Python apply_chat_template
would.
Sourcepub fn try_apply_chat_template_json(
&self,
messages: &[Value],
tools: Option<&[Value]>,
enable_thinking: Option<bool>,
) -> Result<Vec<u32>, String>
pub fn try_apply_chat_template_json( &self, messages: &[Value], tools: Option<&[Value]>, enable_thinking: Option<bool>, ) -> Result<Vec<u32>, String>
Like Self::apply_chat_template_json, but a template that fails
to render is an ERROR instead of a quiet ChatML approximation.
The fallback flattens every message to (role, text): it has no
place for tools, tool_calls or role: "tool". For a plain chat
that is a tolerable degradation; for a request with tools it means
the model never sees the functions and answers as if none were
offered — a failure no client can detect. The server calls this
variant when tools are present and reports the error instead.
Files without a template still take the ChatML path (Ok).
Sourcepub fn render_chat_json(
&self,
messages: &[Value],
tools: Option<&[Value]>,
enable_thinking: Option<bool>,
) -> Option<String>
pub fn render_chat_json( &self, messages: &[Value], tools: Option<&[Value]>, enable_thinking: Option<bool>, ) -> Option<String>
Render the template against JSON-shaped messages (parity surface).
pub fn apply_chat_template_opts( &self, messages: &[(String, String)], enable_thinking: Option<bool>, ) -> Vec<u32>
Sourcepub fn with_bos(&self, ids: Vec<u32>) -> Vec<u32>
pub fn with_bos(&self, ids: Vec<u32>) -> Vec<u32>
Prepend BOS when the tokenizer declares it (llama family).
Sourcepub fn render_chat(&self, messages: &[(String, String)]) -> Option<String>
pub fn render_chat(&self, messages: &[(String, String)]) -> Option<String>
Render the carried template to text (parity-testable surface).
Sourcepub fn render_chat_opts(
&self,
messages: &[(String, String)],
enable_thinking: Option<bool>,
) -> Option<String>
pub fn render_chat_opts( &self, messages: &[(String, String)], enable_thinking: Option<bool>, ) -> Option<String>
Render the carried template to text with explicit thinking mode.
Sourcepub fn vocab_size(&self) -> usize
pub fn vocab_size(&self) -> usize
Vocabulary size.
Sourcepub fn token_to_id(&self, token: &str) -> Option<u32>
pub fn token_to_id(&self, token: &str) -> Option<u32>
Return the ID for an exact token spelling, including added/special tokens. Multimodal prompt preparation uses this to validate the image placeholder against the model configuration.
Sourcepub fn convert_tokens_to_ids(&self, token: &str) -> Option<u32>
pub fn convert_tokens_to_ids(&self, token: &str) -> Option<u32>
Alias matching the HuggingFace tokenizer API used by the official DeepSeek image processor.