1use std::borrow::Cow;
8use std::path::Path;
9
10#[cfg(test)]
11use ferrum_interfaces::vnext::QuantizationGrouping;
12use ferrum_interfaces::vnext::{
13 ElementType, QuantizationSpec, VNextError, WeightComponentPayload, WeightComponentRole,
14 WeightComponentSource, WeightComponentSpec, WeightEncoding,
15};
16#[cfg(test)]
17use ferrum_kernels::marlin_repack::repack_gptq_to_marlin;
18use ferrum_kernels::marlin_repack::{repack_gptq_to_marlin_bytes_into, repack_scales_to_marlin};
19use ferrum_types::Result;
20use half::f16;
21use safetensors::Dtype;
22
23use crate::safetensors_archive::{transcode_dense_bytes, SafetensorsArchive, SafetensorsTensor};
24
25pub const GPTQ_MARLIN_INT4_FORMAT_ID: &str = "quantization.marlin.gptq-int4-symmetric";
26
27pub struct GptqMarlinSafetensorsSource {
30 archive: SafetensorsArchive,
31}
32
33impl GptqMarlinSafetensorsSource {
34 pub fn open(model_dir: impl AsRef<Path>) -> Result<Self> {
35 SafetensorsArchive::open(model_dir).map(Self::new)
36 }
37
38 pub const fn new(archive: SafetensorsArchive) -> Self {
39 Self { archive }
40 }
41
42 pub const fn archive(&self) -> &SafetensorsArchive {
43 &self.archive
44 }
45
46 fn packed_values<'source>(
47 &'source self,
48 component: &WeightComponentSpec,
49 quantization: &QuantizationSpec,
50 ) -> std::result::Result<WeightComponentPayload<'source>, VNextError> {
51 let group_size = usize::try_from(validate_marlin_quantization(component, quantization)?)
52 .map_err(|_| invalid_component(component, "GPTQ group size exceeds address space"))?;
53 let groups = packed_source_groups(component)?;
54 let first_qweight = self.tensor(component, groups[0].qweight)?;
55 let (k, n) = validate_qweight_shape(component, &first_qweight)?;
56 let (expert_count, projections_per_expert) = if groups.len() == 1 {
57 let expected_bytes = component.physical_bytes()?;
58 if usize::try_from(expected_bytes).ok() != Some(first_qweight.bytes().len()) {
59 return Err(invalid_component(
60 component,
61 "qweight byte size differs from the typed packed component",
62 ));
63 }
64 (1, 1)
65 } else {
66 aggregate_axes(component, groups.len(), n, k / 2, "packed")?
67 };
68 let fused_n = n.checked_mul(projections_per_expert).ok_or_else(|| {
69 invalid_component(component, "fused qweight N dimension exceeds address space")
70 })?;
71 let expected_bytes = usize::try_from(component.physical_bytes()?).map_err(|_| {
72 invalid_component(
73 component,
74 "packed component byte size exceeds address space",
75 )
76 })?;
77 let mut bytes = Vec::with_capacity(expected_bytes);
78 let mut source_files = Vec::with_capacity(component.external_names.len());
79
80 for expert_groups in groups.chunks(projections_per_expert) {
81 let mut projections = Vec::with_capacity(projections_per_expert);
82 for group in expert_groups {
83 let qweight = self.tensor(component, group.qweight)?;
84 let shape = validate_qweight_shape(component, &qweight)?;
85 if shape != (k, n) {
86 return Err(invalid_component(
87 component,
88 format!(
89 "qweight source `{}` shape K={}, N={} drifts from K={k}, N={n}",
90 group.qweight, shape.0, shape.1
91 ),
92 ));
93 }
94 let qzeros = self.tensor(component, group.qzeros)?;
95 validate_symmetric_qzeros_shape(component, &qzeros, k, n, group_size)?;
96 source_files.push(qweight.source_file().to_owned());
97 source_files.push(qzeros.source_file().to_owned());
98 if let Some(g_idx_name) = group.g_idx {
99 let g_idx = self.tensor(component, g_idx_name)?;
100 validate_canonical_g_idx(component, &g_idx, k, group_size)?;
101 source_files.push(g_idx.source_file().to_owned());
102 }
103 projections.push(decode_i32(qweight.bytes(), component, "qweight")?);
104 }
105 let fused = concatenate_equal_width_rows(&projections, k / 8, n);
106 let start = bytes.len();
107 let byte_length = fused
108 .len()
109 .checked_mul(std::mem::size_of::<i32>())
110 .ok_or_else(|| {
111 invalid_component(component, "repacked qweight byte length overflows")
112 })?;
113 let end = start.checked_add(byte_length).ok_or_else(|| {
114 invalid_component(component, "aggregate qweight byte length overflows")
115 })?;
116 bytes.resize(end, 0);
117 repack_gptq_to_marlin_bytes_into(&fused, k, fused_n, &mut bytes[start..end]);
118 }
119 debug_assert_eq!(groups.len(), expert_count * projections_per_expert);
120 WeightComponentPayload::from_ordered_sources(
121 component,
122 component.external_names.clone(),
123 source_files,
124 component.dimensions.clone(),
125 ElementType::U8,
126 bytes,
127 )
128 }
129
130 fn scales<'source>(
131 &'source self,
132 component: &WeightComponentSpec,
133 ) -> std::result::Result<WeightComponentPayload<'source>, VNextError> {
134 if component.external_names.is_empty() {
135 return Err(invalid_component(
136 component,
137 "Marlin scales require at least one safetensors source",
138 ));
139 }
140 if component
141 .external_names
142 .iter()
143 .any(|external_name| !external_name.ends_with(".scales"))
144 {
145 return Err(invalid_component(
146 component,
147 "every Marlin scale source must end with .scales",
148 ));
149 }
150 let external_name = &component.external_names[0];
151 let scales = self.tensor(component, external_name)?;
152 let (group_count, n) = validate_scale_shape(component, &scales)?;
153 let (expert_count, projections_per_expert) = if component.external_names.len() == 1 {
154 let mut expected_dimensions = vec![1_u64; component.dimensions.len().saturating_sub(2)];
155 expected_dimensions.extend([n as u64, group_count as u64]);
156 if component.dimensions != expected_dimensions {
157 return Err(invalid_component(
158 component,
159 format!(
160 "typed scale shape {:?} must be {:?} for source shape [{group_count}, {n}]",
161 component.dimensions, expected_dimensions,
162 ),
163 ));
164 }
165 (1, 1)
166 } else {
167 aggregate_axes(
168 component,
169 component.external_names.len(),
170 n,
171 group_count,
172 "scale",
173 )?
174 };
175 let fused_n = n.checked_mul(projections_per_expert).ok_or_else(|| {
176 invalid_component(component, "fused scale N dimension exceeds address space")
177 })?;
178 let expected_bytes = usize::try_from(component.physical_bytes()?).map_err(|_| {
179 invalid_component(component, "scale component byte size exceeds address space")
180 })?;
181 let mut bytes = Vec::with_capacity(expected_bytes);
182 let mut source_files = Vec::with_capacity(component.external_names.len());
183
184 for expert_names in component.external_names.chunks(projections_per_expert) {
185 let mut projections = Vec::with_capacity(projections_per_expert);
186 for external_name in expert_names {
187 let scales = self.tensor(component, external_name)?;
188 let shape = validate_scale_shape(component, &scales)?;
189 if shape != (group_count, n) {
190 return Err(invalid_component(
191 component,
192 format!(
193 "scale source `{external_name}` shape [{}, {}] drifts from [{group_count}, {n}]",
194 shape.0, shape.1
195 ),
196 ));
197 }
198 let source_type = scales.element_type().ok_or_else(|| {
199 invalid_component(
200 component,
201 format!("scales have unsupported dtype {:?}", scales.dtype()),
202 )
203 })?;
204 let f16_bytes = transcode_dense_bytes(
205 scales.bytes(),
206 source_type,
207 ElementType::F16,
208 external_name,
209 None,
210 )?;
211 projections.push(decode_f16(&f16_bytes, component)?);
212 source_files.push(scales.source_file().to_owned());
213 }
214 let fused = concatenate_equal_width_rows(&projections, group_count, n);
215 let repacked = repack_scales_to_marlin(&fused, group_count, fused_n, 1);
216 bytes.extend_from_slice(encode_f16(repacked).as_ref());
217 }
218 debug_assert_eq!(
219 component.external_names.len(),
220 expert_count * projections_per_expert
221 );
222 WeightComponentPayload::from_ordered_sources(
223 component,
224 component.external_names.clone(),
225 source_files,
226 component.dimensions.clone(),
227 ElementType::F16,
228 bytes,
229 )
230 }
231
232 fn tensor<'source>(
233 &'source self,
234 component: &WeightComponentSpec,
235 external_name: &str,
236 ) -> std::result::Result<SafetensorsTensor<'source>, VNextError> {
237 self.archive
238 .tensor(external_name)
239 .map_err(|error| invalid_component(component, error.to_string()))
240 }
241}
242
243impl WeightComponentSource for GptqMarlinSafetensorsSource {
244 fn component<'source>(
245 &'source self,
246 component: &WeightComponentSpec,
247 ) -> std::result::Result<WeightComponentPayload<'source>, VNextError> {
248 match (&component.role, &component.encoding) {
249 (WeightComponentRole::PackedValues, WeightEncoding::Quantized(quantization)) => {
250 self.packed_values(component, quantization)
251 }
252 (
253 WeightComponentRole::Scales,
254 WeightEncoding::Dense {
255 element_type: ElementType::F16,
256 },
257 ) => self.scales(component),
258 (_, WeightEncoding::Dense { .. } | WeightEncoding::DenseAffine { .. }) => {
259 self.archive.component(component)
260 }
261 _ => Err(invalid_component(
262 component,
263 "GPTQ Marlin adapter received an unsupported component encoding",
264 )),
265 }
266 }
267}
268
269#[derive(Clone, Copy)]
270struct PackedSourceGroup<'name> {
271 qweight: &'name str,
272 qzeros: &'name str,
273 g_idx: Option<&'name str>,
274}
275
276fn packed_source_groups(
277 component: &WeightComponentSpec,
278) -> std::result::Result<Vec<PackedSourceGroup<'_>>, VNextError> {
279 if component.external_names.is_empty() {
280 return Err(invalid_component(
281 component,
282 "packed GPTQ values require ordered qweight and qzeros sources",
283 ));
284 }
285 let mut groups = Vec::new();
286 let mut cursor = 0;
287 let mut expected_g_idx_presence = None;
288 while cursor < component.external_names.len() {
289 let qweight = &component.external_names[cursor];
290 let stem = qweight.strip_suffix(".qweight").unwrap_or_default();
291 let Some(qzeros) = component.external_names.get(cursor + 1) else {
292 return Err(invalid_component(
293 component,
294 "each packed GPTQ source group requires qweight followed by qzeros",
295 ));
296 };
297 if stem.is_empty() || qzeros != &format!("{stem}.qzeros") {
298 return Err(invalid_component(
299 component,
300 "packed GPTQ source groups must share one stem and be ordered qweight, qzeros, then optional g_idx",
301 ));
302 }
303 let expected_g_idx = format!("{stem}.g_idx");
304 let g_idx = component
305 .external_names
306 .get(cursor + 2)
307 .filter(|name| name.as_str() == expected_g_idx)
308 .map(String::as_str);
309 let has_g_idx = g_idx.is_some();
310 if expected_g_idx_presence
311 .replace(has_g_idx)
312 .is_some_and(|expected| expected != has_g_idx)
313 {
314 return Err(invalid_component(
315 component,
316 "packed GPTQ source groups cannot mix g_idx presence",
317 ));
318 }
319 groups.push(PackedSourceGroup {
320 qweight,
321 qzeros,
322 g_idx,
323 });
324 cursor += if has_g_idx { 3 } else { 2 };
325 }
326 Ok(groups)
327}
328
329fn aggregate_axes(
330 component: &WeightComponentSpec,
331 source_group_count: usize,
332 source_n: usize,
333 source_tail: usize,
334 label: &str,
335) -> std::result::Result<(usize, usize), VNextError> {
336 if component.dimensions.len() < 3 {
337 return Err(invalid_component(
338 component,
339 format!(
340 "aggregate {label} shape must be [E, projection_axes..., N, physical_K], got {:?}",
341 component.dimensions
342 ),
343 ));
344 }
345 let tail_start = component.dimensions.len() - 2;
346 let expert_count = component.dimensions[0];
347 let typed_n = component.dimensions[tail_start];
348 let typed_tail = component.dimensions[tail_start + 1];
349 let expected_tail = [source_n as u64, source_tail as u64];
350 if [typed_n, typed_tail] != expected_tail {
351 return Err(invalid_component(
352 component,
353 format!(
354 "aggregate {label} tail [{typed_n}, {typed_tail}] must match single-source physical shape {expected_tail:?}"
355 ),
356 ));
357 }
358 let projections_per_expert = component.dimensions[1..tail_start]
359 .iter()
360 .try_fold(1_u64, |count, extent| count.checked_mul(*extent))
361 .ok_or_else(|| {
362 invalid_component(component, "aggregate projection axis product overflows u64")
363 })?;
364 let declared_groups = expert_count
365 .checked_mul(projections_per_expert)
366 .ok_or_else(|| {
367 invalid_component(component, "aggregate source group count overflows u64")
368 })?;
369 if expert_count == 0
370 || projections_per_expert == 0
371 || usize::try_from(declared_groups).ok() != Some(source_group_count)
372 {
373 return Err(invalid_component(
374 component,
375 format!(
376 "aggregate {label} prefix E={expert_count}, projections_per_expert={projections_per_expert} must describe {source_group_count} ordered source groups"
377 ),
378 ));
379 }
380 Ok((
381 usize::try_from(expert_count).map_err(|_| {
382 invalid_component(component, "aggregate expert count exceeds address space")
383 })?,
384 usize::try_from(projections_per_expert).map_err(|_| {
385 invalid_component(
386 component,
387 "aggregate projection count exceeds address space",
388 )
389 })?,
390 ))
391}
392
393fn validate_qweight_shape(
394 component: &WeightComponentSpec,
395 qweight: &SafetensorsTensor<'_>,
396) -> std::result::Result<(usize, usize), VNextError> {
397 if qweight.dtype() != Dtype::I32 {
398 return Err(invalid_component(
399 component,
400 format!("qweight must be I32, got {:?}", qweight.dtype()),
401 ));
402 }
403 let [packed_k, n] = qweight.shape() else {
404 return Err(invalid_component(
405 component,
406 format!(
407 "qweight must have shape [K/8, N], got {:?}",
408 qweight.shape()
409 ),
410 ));
411 };
412 let k = packed_k.checked_mul(8).ok_or_else(|| {
413 invalid_component(component, "qweight K dimension overflows address space")
414 })?;
415 let (k, n) = (
416 usize::try_from(k).map_err(|_| {
417 invalid_component(component, "qweight K dimension exceeds address space")
418 })?,
419 usize::try_from(*n).map_err(|_| {
420 invalid_component(component, "qweight N dimension exceeds address space")
421 })?,
422 );
423 if k % 16 != 0 || n % 16 != 0 || k.checked_mul(n).is_none_or(|elements| elements % 1024 != 0) {
424 return Err(invalid_component(
425 component,
426 format!("qweight shape K={k}, N={n} is not Marlin tile aligned"),
427 ));
428 }
429 Ok((k, n))
430}
431
432fn validate_scale_shape(
433 component: &WeightComponentSpec,
434 scales: &SafetensorsTensor<'_>,
435) -> std::result::Result<(usize, usize), VNextError> {
436 let [group_count, n] = scales.shape() else {
437 return Err(invalid_component(
438 component,
439 format!(
440 "scales must have source shape [K/G, N], got {:?}",
441 scales.shape()
442 ),
443 ));
444 };
445 Ok((
446 usize::try_from(*group_count)
447 .map_err(|_| invalid_component(component, "scale group count exceeds address space"))?,
448 usize::try_from(*n)
449 .map_err(|_| invalid_component(component, "scale N dimension exceeds address space"))?,
450 ))
451}
452
453fn concatenate_equal_width_rows<T: Copy>(
454 parts: &[Vec<T>],
455 row_count: usize,
456 columns_per_part: usize,
457) -> Vec<T> {
458 let mut fused = Vec::with_capacity(row_count * columns_per_part * parts.len());
459 for row in 0..row_count {
460 for part in parts {
461 let start = row * columns_per_part;
462 fused.extend_from_slice(&part[start..start + columns_per_part]);
463 }
464 }
465 fused
466}
467
468fn validate_marlin_quantization(
469 component: &WeightComponentSpec,
470 quantization: &QuantizationSpec,
471) -> std::result::Result<u32, VNextError> {
472 quantization.validate()?;
473 let Some(group_size) = quantization.grouping.fixed_size() else {
474 return Err(invalid_component(
475 component,
476 "typed GPTQ source requires fixed-size quantization groups",
477 ));
478 };
479 if quantization.format_id.as_str() != GPTQ_MARLIN_INT4_FORMAT_ID
480 || quantization.bits_per_weight != 4
481 || quantization.scale_type != ElementType::F16
482 || quantization.zero_point_type.is_some()
483 {
484 return Err(invalid_component(
485 component,
486 "typed GPTQ source requires symmetric INT4 Marlin packing with F16 scales",
487 ));
488 }
489 Ok(group_size)
490}
491
492fn validate_symmetric_qzeros_shape(
493 component: &WeightComponentSpec,
494 qzeros: &SafetensorsTensor<'_>,
495 k: usize,
496 n: usize,
497 group_size: usize,
498) -> std::result::Result<(), VNextError> {
499 if qzeros.dtype() != Dtype::I32
500 || group_size == 0
501 || qzeros.shape() != [k as u64 / group_size as u64, n as u64 / 8]
502 {
503 return Err(invalid_component(
504 component,
505 format!(
506 "qzeros shape/dtype differs from symmetric GPTQ K={k}, N={n}, group_size={group_size}"
507 ),
508 ));
509 }
510 Ok(())
515}
516
517fn validate_canonical_g_idx(
518 component: &WeightComponentSpec,
519 g_idx: &SafetensorsTensor<'_>,
520 k: usize,
521 group_size: usize,
522) -> std::result::Result<(), VNextError> {
523 if g_idx.dtype() != Dtype::I32 || g_idx.shape() != [k as u64] {
524 return Err(invalid_component(
525 component,
526 format!("g_idx must be I32[{k}] for desc_act=false"),
527 ));
528 }
529 let values = decode_i32(g_idx.bytes(), component, "g_idx")?;
530 if values
531 .iter()
532 .enumerate()
533 .any(|(index, value)| *value != (index / group_size) as i32)
534 {
535 return Err(invalid_component(
536 component,
537 "g_idx is activation-ordered; the current typed Marlin ABI requires desc_act=false",
538 ));
539 }
540 Ok(())
541}
542
543fn decode_i32(
544 bytes: &[u8],
545 component: &WeightComponentSpec,
546 label: &str,
547) -> std::result::Result<Vec<i32>, VNextError> {
548 if !bytes.len().is_multiple_of(4) {
549 return Err(invalid_component(
550 component,
551 format!("{label} byte length is not I32 aligned"),
552 ));
553 }
554 Ok(bytes
555 .chunks_exact(4)
556 .map(|word| i32::from_le_bytes([word[0], word[1], word[2], word[3]]))
557 .collect())
558}
559
560fn decode_f16(
561 bytes: &[u8],
562 component: &WeightComponentSpec,
563) -> std::result::Result<Vec<f16>, VNextError> {
564 if !bytes.len().is_multiple_of(2) {
565 return Err(invalid_component(
566 component,
567 "scale byte length is not F16 aligned",
568 ));
569 }
570 Ok(bytes
571 .chunks_exact(2)
572 .map(|word| f16::from_bits(u16::from_le_bytes([word[0], word[1]])))
573 .collect())
574}
575
576fn encode_f16(values: Vec<f16>) -> Cow<'static, [u8]> {
577 Cow::Owned(
578 values
579 .into_iter()
580 .flat_map(|value| value.to_bits().to_le_bytes())
581 .collect::<Vec<_>>(),
582 )
583}
584
585fn invalid_component(component: &WeightComponentSpec, reason: impl AsRef<str>) -> VNextError {
586 VNextError::InvalidExecutionPlan {
587 reason: format!(
588 "GPTQ Marlin component `{}`: {}",
589 component.id,
590 reason.as_ref()
591 ),
592 }
593}
594
595#[cfg(test)]
596mod tests {
597 use std::collections::BTreeMap;
598
599 use ferrum_interfaces::vnext::{QuantizationFormatId, QuantizationPacking, WeightId};
600 use safetensors::tensor::{serialize_to_file, TensorView};
601 use tempfile::tempdir;
602
603 use super::*;
604
605 fn write_fixture(qzeros_word: i32) -> tempfile::TempDir {
606 let directory = tempdir().unwrap();
607 let k = 128_usize;
608 let n = 64_usize;
609 let qweight_words = vec![0x7654_3210_i32; (k / 8) * n];
610 let qzeros_words = vec![qzeros_word; n / 8];
611 let g_idx = (0..k).map(|_| 0_i32).collect::<Vec<_>>();
612 let scales = vec![f16::from_f32(0.5); n];
613 let qweight_bytes = qweight_words
614 .iter()
615 .flat_map(|value| value.to_le_bytes())
616 .collect::<Vec<_>>();
617 let qzeros_bytes = qzeros_words
618 .iter()
619 .flat_map(|value| value.to_le_bytes())
620 .collect::<Vec<_>>();
621 let g_idx_bytes = g_idx
622 .iter()
623 .flat_map(|value| value.to_le_bytes())
624 .collect::<Vec<_>>();
625 let scale_bytes = scales
626 .iter()
627 .flat_map(|value| value.to_bits().to_le_bytes())
628 .collect::<Vec<_>>();
629 let views = BTreeMap::from([
630 (
631 "layer.proj.g_idx",
632 TensorView::new(Dtype::I32, vec![k], &g_idx_bytes).unwrap(),
633 ),
634 (
635 "layer.proj.qweight",
636 TensorView::new(Dtype::I32, vec![k / 8, n], &qweight_bytes).unwrap(),
637 ),
638 (
639 "layer.proj.qzeros",
640 TensorView::new(Dtype::I32, vec![1, n / 8], &qzeros_bytes).unwrap(),
641 ),
642 (
643 "layer.proj.scales",
644 TensorView::new(Dtype::F16, vec![1, n], &scale_bytes).unwrap(),
645 ),
646 ]);
647 serialize_to_file(views, &None, &directory.path().join("model.safetensors")).unwrap();
648 directory
649 }
650
651 struct GateUpFixture {
652 directory: tempfile::TempDir,
653 qweights: [Vec<i32>; 2],
654 scales: [Vec<f16>; 2],
655 k: usize,
656 n: usize,
657 }
658
659 fn write_gate_up_fixture() -> GateUpFixture {
660 let directory = tempdir().unwrap();
661 let k = 128_usize;
662 let n = 16_usize;
663 let qweights = [
664 (0..(k / 8) * n)
665 .map(|index| (index as u32).wrapping_mul(0x1020_4081) as i32)
666 .collect::<Vec<_>>(),
667 (0..(k / 8) * n)
668 .map(|index| {
669 (index as u32)
670 .wrapping_mul(0x0810_2041)
671 .wrapping_add(0x7654_3210) as i32
672 })
673 .collect::<Vec<_>>(),
674 ];
675 let scales = [
676 (0..n)
677 .map(|index| f16::from_f32(index as f32 + 1.0))
678 .collect::<Vec<_>>(),
679 (0..n)
680 .map(|index| f16::from_f32(index as f32 + 101.0))
681 .collect::<Vec<_>>(),
682 ];
683 let qweight_bytes = qweights.each_ref().map(|values| {
684 values
685 .iter()
686 .flat_map(|value| value.to_le_bytes())
687 .collect::<Vec<_>>()
688 });
689 let scale_bytes = scales.each_ref().map(|values| {
690 values
691 .iter()
692 .flat_map(|value| value.to_bits().to_le_bytes())
693 .collect::<Vec<_>>()
694 });
695 let qzeros = vec![0x8888_8888_u32 as i32; n / 8];
696 let qzeros_bytes = qzeros
697 .iter()
698 .flat_map(|value| value.to_le_bytes())
699 .collect::<Vec<_>>();
700 let g_idx = vec![0_i32; k];
701 let g_idx_bytes = g_idx
702 .iter()
703 .flat_map(|value| value.to_le_bytes())
704 .collect::<Vec<_>>();
705 let views = BTreeMap::from([
706 (
707 "layer.gate.g_idx",
708 TensorView::new(Dtype::I32, vec![k], &g_idx_bytes).unwrap(),
709 ),
710 (
711 "layer.gate.qweight",
712 TensorView::new(Dtype::I32, vec![k / 8, n], &qweight_bytes[0]).unwrap(),
713 ),
714 (
715 "layer.gate.qzeros",
716 TensorView::new(Dtype::I32, vec![1, n / 8], &qzeros_bytes).unwrap(),
717 ),
718 (
719 "layer.gate.scales",
720 TensorView::new(Dtype::F16, vec![1, n], &scale_bytes[0]).unwrap(),
721 ),
722 (
723 "layer.up.g_idx",
724 TensorView::new(Dtype::I32, vec![k], &g_idx_bytes).unwrap(),
725 ),
726 (
727 "layer.up.qweight",
728 TensorView::new(Dtype::I32, vec![k / 8, n], &qweight_bytes[1]).unwrap(),
729 ),
730 (
731 "layer.up.qzeros",
732 TensorView::new(Dtype::I32, vec![1, n / 8], &qzeros_bytes).unwrap(),
733 ),
734 (
735 "layer.up.scales",
736 TensorView::new(Dtype::F16, vec![1, n], &scale_bytes[1]).unwrap(),
737 ),
738 ]);
739 serialize_to_file(views, &None, &directory.path().join("model.safetensors")).unwrap();
740 GateUpFixture {
741 directory,
742 qweights,
743 scales,
744 k,
745 n,
746 }
747 }
748
749 fn quantization() -> QuantizationSpec {
750 QuantizationSpec {
751 format_id: QuantizationFormatId::new(GPTQ_MARLIN_INT4_FORMAT_ID).unwrap(),
752 bits_per_weight: 4,
753 grouping: QuantizationGrouping::fixed(128),
754 packing: QuantizationPacking::Tiled,
755 scale_type: ElementType::F16,
756 zero_point_type: None,
757 }
758 }
759
760 fn packed_component() -> WeightComponentSpec {
761 WeightComponentSpec {
762 id: WeightId::new("component.layer.proj.packed").unwrap(),
763 role: WeightComponentRole::PackedValues,
764 external_names: vec![
765 "layer.proj.qweight".to_owned(),
766 "layer.proj.qzeros".to_owned(),
767 "layer.proj.g_idx".to_owned(),
768 ],
769 dimensions: vec![4096],
770 encoding: WeightEncoding::Quantized(quantization()),
771 required: true,
772 }
773 }
774
775 #[test]
776 fn repacks_valid_symmetric_gptq_components_once_at_source_boundary() {
777 let directory = write_fixture(0x8888_8888_u32 as i32);
778 let source = GptqMarlinSafetensorsSource::open(directory.path()).unwrap();
779 let packed = packed_component();
780 let payload = source.component(&packed).unwrap();
781 assert_eq!(payload.bytes().len(), 4096);
782 assert_eq!(payload.external_names(), packed.external_names);
783
784 let scales = WeightComponentSpec {
785 id: WeightId::new("component.layer.proj.scales").unwrap(),
786 role: WeightComponentRole::Scales,
787 external_names: vec!["layer.proj.scales".to_owned()],
788 dimensions: vec![64, 1],
789 encoding: WeightEncoding::Dense {
790 element_type: ElementType::F16,
791 },
792 required: true,
793 };
794 let payload = source.component(&scales).unwrap();
795 assert_eq!(payload.bytes().len(), 128);
796 assert_eq!(payload.dimensions(), [64, 1]);
797 }
798
799 #[test]
800 fn symmetric_qzeros_convention_does_not_change_marlin_payload() {
801 let code7 = write_fixture(0x7777_7777);
802 let code8 = write_fixture(0x8888_8888_u32 as i32);
803 let source7 = GptqMarlinSafetensorsSource::open(code7.path()).unwrap();
804 let source8 = GptqMarlinSafetensorsSource::open(code8.path()).unwrap();
805 let component = packed_component();
806
807 assert_eq!(
808 source7.component(&component).unwrap().bytes(),
809 source8.component(&component).unwrap().bytes()
810 );
811 }
812
813 #[test]
814 fn aggregate_gate_up_fuses_raw_columns_before_marlin_repack() {
815 let fixture = write_gate_up_fixture();
816 let source = GptqMarlinSafetensorsSource::open(fixture.directory.path()).unwrap();
817 let packed = WeightComponentSpec {
818 id: WeightId::new("component.layer.gate_up.packed").unwrap(),
819 role: WeightComponentRole::PackedValues,
820 external_names: vec![
821 "layer.gate.qweight".to_owned(),
822 "layer.gate.qzeros".to_owned(),
823 "layer.gate.g_idx".to_owned(),
824 "layer.up.qweight".to_owned(),
825 "layer.up.qzeros".to_owned(),
826 "layer.up.g_idx".to_owned(),
827 ],
828 dimensions: vec![1, 2, fixture.n as u64, (fixture.k / 2) as u64],
829 encoding: WeightEncoding::Quantized(quantization()),
830 required: true,
831 };
832 let raw_fused = concatenate_equal_width_rows(&fixture.qweights, fixture.k / 8, fixture.n);
833 let expected = repack_gptq_to_marlin(&raw_fused, fixture.k, fixture.n * 2)
834 .into_iter()
835 .flat_map(i32::to_le_bytes)
836 .collect::<Vec<_>>();
837 let independently_repacked = fixture
838 .qweights
839 .iter()
840 .flat_map(|values| {
841 repack_gptq_to_marlin(values, fixture.k, fixture.n)
842 .into_iter()
843 .flat_map(i32::to_le_bytes)
844 })
845 .collect::<Vec<_>>();
846 assert_ne!(expected, independently_repacked);
847 let payload = source.component(&packed).unwrap();
848 assert_eq!(payload.bytes(), expected);
849 assert_eq!(payload.external_names(), packed.external_names);
850
851 let scales = WeightComponentSpec {
852 id: WeightId::new("component.layer.gate_up.scales").unwrap(),
853 role: WeightComponentRole::Scales,
854 external_names: vec!["layer.gate.scales".to_owned(), "layer.up.scales".to_owned()],
855 dimensions: vec![1, 2, fixture.n as u64, 1],
856 encoding: WeightEncoding::Dense {
857 element_type: ElementType::F16,
858 },
859 required: true,
860 };
861 let raw_fused_scales = concatenate_equal_width_rows(&fixture.scales, 1, fixture.n);
862 let expected_scales = encode_f16(repack_scales_to_marlin(
863 &raw_fused_scales,
864 1,
865 fixture.n * 2,
866 1,
867 ));
868 let independently_repacked_scales = fixture
869 .scales
870 .iter()
871 .flat_map(|values| {
872 repack_scales_to_marlin(values, 1, fixture.n, 1)
873 .into_iter()
874 .flat_map(|value| value.to_bits().to_le_bytes())
875 })
876 .collect::<Vec<_>>();
877 assert_ne!(expected_scales.as_ref(), independently_repacked_scales);
878 let payload = source.component(&scales).unwrap();
879 assert_eq!(payload.bytes(), expected_scales.as_ref());
880 assert_eq!(payload.external_names(), scales.external_names);
881 }
882
883 #[test]
884 fn aggregate_experts_without_projection_axis_repack_independently() {
885 let fixture = write_gate_up_fixture();
886 let source = GptqMarlinSafetensorsSource::open(fixture.directory.path()).unwrap();
887 let packed = WeightComponentSpec {
888 id: WeightId::new("component.layer.experts.packed").unwrap(),
889 role: WeightComponentRole::PackedValues,
890 external_names: vec![
891 "layer.gate.qweight".to_owned(),
892 "layer.gate.qzeros".to_owned(),
893 "layer.gate.g_idx".to_owned(),
894 "layer.up.qweight".to_owned(),
895 "layer.up.qzeros".to_owned(),
896 "layer.up.g_idx".to_owned(),
897 ],
898 dimensions: vec![2, fixture.n as u64, (fixture.k / 2) as u64],
899 encoding: WeightEncoding::Quantized(quantization()),
900 required: true,
901 };
902 let expected = fixture
903 .qweights
904 .iter()
905 .flat_map(|values| {
906 repack_gptq_to_marlin(values, fixture.k, fixture.n)
907 .into_iter()
908 .flat_map(i32::to_le_bytes)
909 })
910 .collect::<Vec<_>>();
911 let payload = source.component(&packed).unwrap();
912 assert_eq!(payload.bytes(), expected);
913 assert_eq!(payload.dimensions(), packed.dimensions);
914
915 let scales = WeightComponentSpec {
916 id: WeightId::new("component.layer.experts.scales").unwrap(),
917 role: WeightComponentRole::Scales,
918 external_names: vec!["layer.gate.scales".to_owned(), "layer.up.scales".to_owned()],
919 dimensions: vec![2, fixture.n as u64, 1],
920 encoding: WeightEncoding::Dense {
921 element_type: ElementType::F16,
922 },
923 required: true,
924 };
925 let expected_scales = fixture
926 .scales
927 .iter()
928 .flat_map(|values| {
929 repack_scales_to_marlin(values, 1, fixture.n, 1)
930 .into_iter()
931 .flat_map(|value| value.to_bits().to_le_bytes())
932 })
933 .collect::<Vec<_>>();
934 let payload = source.component(&scales).unwrap();
935 assert_eq!(payload.bytes(), expected_scales);
936 assert_eq!(payload.dimensions(), scales.dimensions);
937 }
938}