pictor_model/convert/
common.rs1use std::path::Path;
14
15use anyhow::Context;
16use serde_json::Value;
17
18use pictor_core::gguf::tensor_info::keys;
19use pictor_core::gguf::writer::{GgufWriter, MetadataWriteValue};
20use pictor_core::quant_ternary::{BlockTQ2_0_g128, BLOCK_TQ2_0_G128_BYTES};
21
22#[derive(Debug, Clone, Default)]
28pub struct ConvertStats {
29 pub n_tensors: usize,
31 pub n_ternary: usize,
33 pub n_fp32: usize,
35 pub n_bf16: usize,
37 pub n_f16: usize,
39 pub output_bytes: usize,
41}
42
43pub fn read_config_json(config_path: &Path) -> anyhow::Result<Value> {
49 let raw = std::fs::read_to_string(config_path)
50 .with_context(|| format!("reading {:?}", config_path))?;
51 let value: Value =
52 serde_json::from_str(&raw).with_context(|| format!("parsing {:?}", config_path))?;
53 Ok(value)
54}
55
56pub fn write_metadata(
62 writer: &mut GgufWriter,
63 config: &Value,
64 model_name: &str,
65) -> anyhow::Result<()> {
66 writer.add_metadata(
68 keys::GENERAL_ARCHITECTURE,
69 MetadataWriteValue::Str("qwen3".to_string()),
70 );
71
72 writer.add_metadata(
74 keys::GENERAL_NAME,
75 MetadataWriteValue::Str(model_name.to_string()),
76 );
77
78 writer.add_metadata(
80 "general.quantization_version",
81 MetadataWriteValue::Str("TQ2_0_G128".to_string()),
82 );
83
84 let u32_keys = [
86 (keys::LLM_BLOCK_COUNT, "num_hidden_layers"),
87 (keys::LLM_EMBEDDING_LENGTH, "hidden_size"),
88 (keys::LLM_FEED_FORWARD_LENGTH, "intermediate_size"),
89 (keys::LLM_ATTENTION_HEAD_COUNT, "num_attention_heads"),
90 (keys::LLM_ATTENTION_HEAD_COUNT_KV, "num_key_value_heads"),
91 (keys::LLM_CONTEXT_LENGTH, "max_position_embeddings"),
92 (keys::LLM_VOCAB_SIZE, "vocab_size"),
93 ];
94 for (gguf_key, json_key) in &u32_keys {
95 if let Some(val) = config.get(*json_key).and_then(Value::as_u64) {
96 writer.add_metadata(gguf_key, MetadataWriteValue::U32(val as u32));
97 } else {
98 tracing::warn!(json_key, "missing or non-u64 field in config.json");
99 }
100 }
101
102 if let Some(val) = config.get("head_dim").and_then(Value::as_u64) {
108 writer.add_metadata(
109 keys::LLM_ATTENTION_KEY_LENGTH,
110 MetadataWriteValue::U32(val as u32),
111 );
112 }
113
114 if let Some(eps) = config.get("rms_norm_eps").and_then(Value::as_f64) {
116 writer.add_metadata(
117 keys::LLM_ATTENTION_LAYER_NORM_RMS_EPSILON,
118 MetadataWriteValue::F32(eps as f32),
119 );
120 }
121
122 let rope_theta = resolve_rope_theta(config);
129 writer.add_metadata(
130 keys::LLM_ROPE_FREQ_BASE,
131 MetadataWriteValue::F32(rope_theta as f32),
132 );
133
134 if let Some(rp) = config.get("rope_parameters").and_then(Value::as_object) {
141 let rope_type = rp.get("rope_type").and_then(Value::as_str).unwrap_or("");
142 if rope_type.eq_ignore_ascii_case("yarn") {
143 let factor = rp.get("factor").and_then(Value::as_f64);
144 let original_max_pos = rp
145 .get("original_max_position_embeddings")
146 .and_then(Value::as_u64);
147 tracing::info!(
148 ?factor,
149 ?original_max_pos,
150 "YARN rope_parameters detected; GGUF YARN metadata not plumbed, relying on architecture defaults"
151 );
152 }
153 }
154
155 Ok(())
156}
157
158fn resolve_rope_theta(config: &Value) -> f64 {
165 if let Some(v) = config.get("rope_theta").and_then(Value::as_f64) {
166 return v;
167 }
168 if let Some(v) = config
169 .get("rope_parameters")
170 .and_then(|rp| rp.get("rope_theta"))
171 .and_then(Value::as_f64)
172 {
173 return v;
174 }
175 tracing::warn!(
176 "config.json missing both `rope_theta` and `rope_parameters.rope_theta`; \
177 falling back to default 10000.0"
178 );
179 10000.0
180}
181
182pub fn pad_to_multiple_of_128(f32_data: &[f32]) -> Vec<f32> {
187 let len = f32_data.len();
188 let remainder = len % 128;
189 if remainder == 0 {
190 f32_data.to_vec()
191 } else {
192 let padded_len = len + (128 - remainder);
193 let mut padded = f32_data.to_vec();
194 padded.resize(padded_len, 0.0_f32);
195 padded
196 }
197}
198
199pub fn blocks_to_bytes(blocks: &[BlockTQ2_0_g128]) -> Vec<u8> {
209 let total = blocks.len() * BLOCK_TQ2_0_G128_BYTES;
210 let bytes: &[u8] = unsafe { std::slice::from_raw_parts(blocks.as_ptr() as *const u8, total) };
212 bytes.to_vec()
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218 use serde_json::json;
219
220 #[test]
221 fn pad_aligned_is_identity() {
222 let v = vec![1.0_f32; 128];
223 assert_eq!(pad_to_multiple_of_128(&v), v);
224 }
225
226 #[test]
227 fn pad_extends_to_next_block() {
228 let v = vec![1.0_f32; 130];
229 let padded = pad_to_multiple_of_128(&v);
230 assert_eq!(padded.len(), 256);
231 assert_eq!(&padded[..130], &v[..]);
232 assert!(padded[130..].iter().all(|&x| x == 0.0));
233 }
234
235 #[test]
236 fn empty_input_stays_empty() {
237 let v: Vec<f32> = Vec::new();
238 assert!(pad_to_multiple_of_128(&v).is_empty());
239 }
240
241 #[test]
242 fn rope_theta_top_level_wins() {
243 let cfg = json!({
245 "rope_theta": 500000.0,
246 });
247 assert_eq!(resolve_rope_theta(&cfg), 500000.0);
248 }
249
250 #[test]
251 fn rope_theta_nested_under_rope_parameters() {
252 let cfg = json!({
254 "rope_parameters": {
255 "factor": 4.0,
256 "original_max_position_embeddings": 8192,
257 "rope_theta": 1_000_000.0,
258 "rope_type": "yarn",
259 },
260 });
261 assert_eq!(resolve_rope_theta(&cfg), 1_000_000.0);
262 }
263
264 #[test]
265 fn rope_theta_top_level_takes_precedence_over_nested() {
266 let cfg = json!({
268 "rope_theta": 250000.0,
269 "rope_parameters": {
270 "rope_theta": 1_000_000.0,
271 "rope_type": "yarn",
272 },
273 });
274 assert_eq!(resolve_rope_theta(&cfg), 250000.0);
275 }
276
277 #[test]
278 fn rope_theta_fallback_when_missing() {
279 let cfg = json!({
281 "hidden_size": 2048,
282 });
283 assert_eq!(resolve_rope_theta(&cfg), 10000.0);
284 }
285}