1use std::collections::HashMap;
18use std::path::{Path, PathBuf};
19
20use memmap2::Mmap;
21
22use crate::flatbuf::FlatBuffer;
23use crate::metadata::AttentionPattern;
24use crate::protomin::Proto;
25use crate::source::{ModelSource, TensorDtype, TensorReader};
26use crate::tokenizer::TokenizerSpec;
27use crate::{FormatError, ModelMetadata, Result};
28
29fn bad(what: impl Into<String>) -> FormatError {
30 FormatError::Safetensors(format!("tflite: {}", what.into()))
31}
32
33const TFLITE_F32: u8 = 0;
35const TFLITE_INT8: u8 = 9;
36
37struct RawTensor {
39 name: String,
40 shape: Vec<usize>,
41 dtype: u8,
42 buffer: usize,
43}
44
45#[derive(Clone, Copy)]
47struct BufferSpan {
48 offset: usize,
49 size: usize,
50}
51
52struct QuantInfo {
54 scale_buffer: usize,
55 rows: usize,
56}
57
58fn map_name(odml: &str) -> Option<String> {
62 let rest = odml.strip_prefix("transformer.")?;
63 if rest == "embedder.input_embedding.w" {
64 return Some("model.embed_tokens.weight".into());
65 }
66 if rest == "final_norm.scale" {
67 return Some("model.norm.weight".into());
68 }
69 let layer = rest.strip_prefix("layer_")?;
70 let (idx, sub) = layer.split_once('.')?;
71 let p = format!("model.layers.{idx}");
72 let mapped = match sub {
73 "attn.q.w" => "self_attn.q_proj.weight",
74 "attn.k.w" => "self_attn.k_proj.weight",
75 "attn.v.w" => "self_attn.v_proj.weight",
76 "attn.attn_vec_einsum.w" => "self_attn.o_proj.weight",
77 "attn.q_norm.scale" => "self_attn.q_norm.weight",
78 "attn.k_norm.scale" => "self_attn.k_norm.weight",
79 "pre_attention_norm.scale" => "input_layernorm.weight",
80 "post_attention_norm.scale" => "post_attention_layernorm.weight",
81 "pre_ffw_norm.scale" => "pre_feedforward_layernorm.weight",
82 "post_ffw_norm.scale" => "post_feedforward_layernorm.weight",
83 "mlp.ff_gate.w" => "mlp.gate_proj.weight",
84 "mlp.ff1.w" | "mlp.ffn.w" => "mlp.up_proj.weight",
85 "mlp.linear.w" => "mlp.down_proj.weight",
86 _ => return None, };
88 Some(format!("{p}.{mapped}"))
89}
90
91#[derive(Default, Debug)]
94struct LlmParameters {
95 vocab_size: usize,
96 hidden_size: usize,
97 intermediate_size: usize,
98 num_hidden_layers: usize,
99 num_attention_heads: usize,
100 num_key_value_heads: usize,
101 head_dim: usize,
102 sliding_window: Option<usize>,
103 sliding_window_pattern: usize,
104 rope_theta: f64,
105 rope_local_theta: f64,
106 query_pre_attn_scalar: Option<f64>,
107 eos_token_ids: Vec<u32>,
108 bos_token_id: Option<u32>,
109}
110
111fn parse_llm_parameters(buf: &[u8]) -> Result<LlmParameters> {
115 let mut out = LlmParameters::default();
116 let mut p = Proto::new(buf);
117 while let Some((field, wire)) = p.tag()? {
118 match (field, wire) {
119 (1, 2) => {
120 let sub = p.bytes()?;
121 let mut sp = Proto::new(sub);
122 while let Some((f, w)) = sp.tag()? {
123 match (f, w) {
124 (3, 0) => out.hidden_size = sp.varint()? as usize,
125 (4, 0) => out.intermediate_size = sp.varint()? as usize,
126 (5, 0) => out.head_dim = sp.varint()? as usize,
127 (6, 0) => out.num_attention_heads = sp.varint()? as usize,
128 (7, 0) => out.num_hidden_layers = sp.varint()? as usize,
129 (9, 0) => out.num_key_value_heads = sp.varint()? as usize,
130 (_, w) => sp.skip(w)?,
131 }
132 }
133 }
134 (2, 0) => out.vocab_size = p.varint()? as usize,
135 (4, 0) => out.bos_token_id = Some(p.varint()? as u32),
136 (_, w) => p.skip(w)?,
137 }
138 }
139 Ok(out)
140}
141
142pub struct TfliteSource {
144 _mmap: Mmap,
145 spans: Vec<BufferSpan>,
146 tensors: HashMap<String, (usize, Option<QuantInfo>)>,
148 raws: Vec<RawTensor>,
149 metadata: ModelMetadata,
150 tokenizer_json: PathBuf,
151 added_tokens: HashMap<u32, String>,
152}
153
154impl TfliteSource {
155 pub fn load(path: impl AsRef<Path>) -> Result<Self> {
157 Self::load_at(path.as_ref(), 0)
158 }
159
160 pub(crate) fn load_at(path: &Path, base: usize) -> Result<Self> {
165 Self::load_at_with_spm(path, base, None)
166 }
167
168 pub(crate) fn load_at_with_spm(
172 path: &Path,
173 base: usize,
174 spm_override: Option<&[u8]>,
175 ) -> Result<Self> {
176 let file = std::fs::File::open(path)?;
177 let mmap = unsafe { Mmap::map(&file)? };
178 let fb = FlatBuffer::new(&mmap[base..], Some(b"TFL3"))?;
179 let root = fb.root();
180
181 let buffer_tables = fb.table_vector(root, 4)?;
184 let mut spans = Vec::with_capacity(buffer_tables.len());
185 for &b in &buffer_tables {
186 let offset = fb.scalar_u64(b, 1).unwrap_or(0) as usize;
187 let size = fb.scalar_u64(b, 2).unwrap_or(0) as usize;
188 spans.push(BufferSpan { offset: base + offset, size });
189 }
190
191 let subgraphs = fb.table_vector(root, 2)?;
193 let sg = subgraphs
194 .first()
195 .ok_or_else(|| bad("no subgraphs"))?;
196 let tensor_tables = fb.table_vector(*sg, 0)?;
197 let mut raws = Vec::with_capacity(tensor_tables.len());
198 for &t in &tensor_tables {
199 let name = fb.string(t, 3)?.unwrap_or_default();
200 let (sp, sn) = fb.vector(t, 0)?;
201 let shape = match sp {
202 Some(p) => fb
203 .i32_slice(p, sn)?
204 .into_iter()
205 .map(|d| d as usize)
206 .collect(),
207 None => Vec::new(),
208 };
209 let dtype = fb.scalar_u8(t, 1).unwrap_or(TFLITE_F32);
210 let buffer = fb.scalar_u32(t, 2).unwrap_or(0) as usize;
211 raws.push(RawTensor { name, shape, dtype, buffer });
212 }
213
214 let mut meta_buffers: HashMap<String, usize> = HashMap::new();
216 for &m in &fb.table_vector(root, 6)? {
217 if let Some(name) = fb.string(m, 0)? {
218 let idx = fb.scalar_u32(m, 1).unwrap_or(0) as usize;
219 meta_buffers.insert(name, idx);
220 }
221 }
222
223 let params = {
225 let idx = meta_buffers
226 .get("odml.infra.proto.LlmParameters")
227 .ok_or_else(|| bad("no LlmParameters metadata"))?;
228 let bytes = read_span(&mmap, spans[*idx])?;
229 parse_llm_parameters(bytes)?
230 };
231 let metadata = metadata_from_params(¶ms)?;
232
233 let spm_bytes: Vec<u8> = match spm_override {
236 Some(b) => b.to_vec(),
237 None => {
238 let spm_idx = meta_buffers
239 .get("spm_vocab_model")
240 .ok_or_else(|| bad("no spm_vocab_model metadata"))?;
241 read_span(&mmap, spans[*spm_idx])?.to_vec()
242 }
243 };
244 let spm_path = path.with_extension("extracted.spm.model");
245 if !spm_path.exists() {
246 std::fs::write(&spm_path, &spm_bytes)?;
247 }
248 let tokenizer_json = crate::spm::ensure_tokenizer_json_from_spm(&spm_path)?;
249 let added_tokens = crate::spm::spm_added_tokens(&spm_path)?;
250
251 let mut tensors: HashMap<String, (usize, Option<QuantInfo>)> = HashMap::new();
253 for (i, raw) in raws.iter().enumerate() {
254 let Some(hf) = map_name(&raw.name) else { continue };
255 let quant = if raw.dtype == TFLITE_INT8 {
256 let scale_name = format!("{}_quantized_scale", raw.name);
257 let scale_idx = raws
258 .iter()
259 .position(|r| r.name == scale_name)
260 .ok_or_else(|| bad(format!("missing scales for {}", raw.name)))?;
261 Some(QuantInfo {
262 scale_buffer: raws[scale_idx].buffer,
263 rows: raws[scale_idx].shape.first().copied().unwrap_or(0),
264 })
265 } else {
266 None
267 };
268 tensors.insert(hf, (i, quant));
269 }
270
271 Ok(TfliteSource {
272 _mmap: mmap,
273 spans,
274 tensors,
275 raws,
276 metadata,
277 tokenizer_json,
278 added_tokens,
279 })
280 }
281}
282
283fn read_span(mmap: &Mmap, span: BufferSpan) -> Result<&[u8]> {
284 mmap.get(span.offset..span.offset + span.size)
285 .ok_or_else(|| bad(format!("buffer out of bounds @{}+{}", span.offset, span.size)))
286}
287
288fn metadata_from_params(p: &LlmParameters) -> Result<ModelMetadata> {
289 if p.hidden_size == 0 || p.num_hidden_layers == 0 || p.vocab_size == 0 {
290 return Err(FormatError::MissingField(format!(
291 "LlmParameters incomplete: {p:?}"
292 )));
293 }
294 Ok(ModelMetadata {
295 architecture: "gemma3_text".to_string(),
296 hidden_size: p.hidden_size,
297 intermediate_size: p.intermediate_size,
298 num_hidden_layers: p.num_hidden_layers,
299 num_attention_heads: p.num_attention_heads,
300 num_key_value_heads: p.num_key_value_heads,
301 vocab_size: p.vocab_size,
302 max_position_embeddings: 32768,
303 rms_norm_eps: 1e-6,
304 rope_theta: if p.rope_theta > 0.0 { p.rope_theta } else { 1_000_000.0 },
305 tie_word_embeddings: true, head_dim: p.head_dim,
307 attention_bias: false,
308 bos_token_id: p.bos_token_id,
309 eos_token_ids: p.eos_token_ids.clone(),
310 vision: None,
311 attention_pattern: AttentionPattern {
312 sliding_window: p.sliding_window.or(Some(512)),
313 pattern: if p.sliding_window_pattern > 0 { p.sliding_window_pattern } else { 6 },
314 rope_local_theta: if p.rope_local_theta > 0.0 { p.rope_local_theta } else { 10_000.0 },
315 query_pre_attn_scalar: p.query_pre_attn_scalar,
316 max_window_layers: None,
317 },
318 activation: crate::metadata::Activation::GeluTanh,
320 rope_scaling: crate::metadata::RopeScaling::None,
321 })
322}
323
324impl ModelSource for TfliteSource {
325 fn metadata(&self) -> &ModelMetadata {
326 &self.metadata
327 }
328
329 fn tensor_names(&self) -> Vec<String> {
330 self.tensors.keys().cloned().collect()
331 }
332
333 fn open_tensor(&self, name: &str) -> Result<TensorReader<'_>> {
334 let (idx, quant) = self
335 .tensors
336 .get(name)
337 .ok_or_else(|| FormatError::TensorNotFound(name.to_string()))?;
338 let raw = &self.raws[*idx];
339 let data = read_span(&self._mmap, self.spans[raw.buffer])?;
340 let n: usize = raw.shape.iter().product();
341 let out_dim = match quant {
345 Some(q) => q.rows,
346 None => logical_out_dim(&self.metadata, name, n)?,
347 };
348 let logical = vec![n / out_dim, out_dim];
349
350 match raw.dtype {
351 TFLITE_F32 => {
352 let values: Vec<f32> = data
353 .chunks_exact(4)
354 .take(n)
355 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
356 .collect();
357 let (values, shape) = reorient(&self.metadata, name, values, &logical)?;
358 let bytes: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
359 Ok(TensorReader::owned(name.to_string(), shape, bytes))
360 }
361 TFLITE_INT8 => {
362 let q = quant
363 .as_ref()
364 .ok_or_else(|| bad(format!("int8 tensor {} without scales", raw.name)))?;
365 let scales_raw = read_span(&self._mmap, self.spans[q.scale_buffer])?;
366 let scales: Vec<f32> = scales_raw
367 .chunks_exact(4)
368 .take(q.rows)
369 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
370 .collect();
371 let values = dequant_i8(data, &scales, &logical)?;
372 let (values, shape) = reorient(&self.metadata, name, values, &logical)?;
373 let bytes: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
374 Ok(TensorReader::owned(name.to_string(), shape, bytes))
375 }
376 other => Err(FormatError::UnsupportedDtype {
377 tensor: raw.name.clone(),
378 dtype: format!("tflite type {other} (int4 packed: follow-up)"),
379 }),
380 }
381 }
382
383 fn tokenizer(&self) -> Result<TokenizerSpec> {
384 Ok(TokenizerSpec {
385 tokenizer_json: self.tokenizer_json.clone(),
386 added_tokens: self.added_tokens.clone(),
387 chat_template: None,
388 add_bos: None,
389 })
390 }
391
392 fn sampler_defaults(&self) -> Option<crate::SamplerConfig> {
393 None
394 }
395}
396
397fn logical_out_dim(m: &ModelMetadata, hf_name: &str, n: usize) -> Result<usize> {
400 if hf_name.ends_with("_layernorm.weight")
401 || hf_name.ends_with("_norm.weight")
402 || hf_name == "model.norm.weight"
403 {
404 return Ok(n); }
406 if hf_name == "model.embed_tokens.weight" || hf_name == "lm_head.weight" {
407 return Ok(m.hidden_size); }
409 Ok(if hf_name.ends_with("q_proj.weight") {
410 m.num_attention_heads * m.head_dim
411 } else if hf_name.ends_with("k_proj.weight") || hf_name.ends_with("v_proj.weight") {
412 m.num_key_value_heads * m.head_dim
413 } else if hf_name.ends_with("o_proj.weight") {
414 m.hidden_size
415 } else if hf_name.ends_with("gate_proj.weight") || hf_name.ends_with("up_proj.weight") {
416 m.intermediate_size
417 } else if hf_name.ends_with("down_proj.weight") {
418 m.hidden_size
419 } else {
420 return Err(bad(format!("no logical shape rule for {hf_name}")));
421 })
422}
423
424fn reorient(
427 metadata: &ModelMetadata,
428 hf_name: &str,
429 values: Vec<f32>,
430 logical: &[usize],
431) -> Result<(Vec<f32>, Vec<usize>)> {
432 if hf_name == "model.embed_tokens.weight" || hf_name == "lm_head.weight" {
433 return Ok((values, logical.to_vec()));
434 }
435 if logical[0] == 1 || logical[1] == 1 {
436 let shape = if logical[0] == 1 { vec![logical[1]] } else { logical.to_vec() };
438 return Ok((values, shape));
439 }
440 if logical[0] == logical[1] && logical[0] == metadata.hidden_size && hf_name.ends_with("norm.weight") {
441 return Ok((values, logical.to_vec()));
442 }
443 let (rows, cols) = (logical[0], logical[1]);
444 let mut out = vec![0f32; values.len()];
445 for r in 0..rows {
446 for c in 0..cols {
447 out[c * rows + r] = values[r * cols + c];
448 }
449 }
450 Ok((out, vec![cols, rows]))
451}
452
453fn dequant_i8(data: &[u8], scales: &[f32], logical: &[usize]) -> Result<Vec<f32>> {
456 let (rows, cols) = (logical[0], logical[1]);
457 if scales.len() != cols {
458 return Err(bad(format!(
459 "scale count {} != out dim {cols}",
460 scales.len()
461 )));
462 }
463 let mut out = Vec::with_capacity(rows * cols);
464 for r in 0..rows {
465 for c in 0..cols {
466 let q = data[r * cols + c] as i8;
467 out.push(q as f32 * scales[c]);
468 }
469 }
470 Ok(out)
471}