|
20 | 20 |
|
21 | 21 | use super::bgz17_bridge::Base17; |
22 | 22 | use super::gguf::{self, GgufFile, TensorInfo, GgmlType}; |
23 | | -use std::io::{Read, Seek, Write}; |
| 23 | +use std::io::{Read, Seek, SeekFrom, Write}; |
24 | 24 |
|
25 | 25 | // ============================================================================ |
26 | 26 | // Layer classification |
@@ -630,6 +630,248 @@ pub fn stream_index_gguf<R: Read + Seek, W: Write>( |
630 | 630 | Ok(stats) |
631 | 631 | } |
632 | 632 |
|
| 633 | +/// Maximum f32 elements before switching to row-wise streaming (512 M elements = 2 GB f32). |
| 634 | +const LARGE_TENSOR_THRESHOLD: usize = 512 * 1024 * 1024; |
| 635 | + |
| 636 | +/// Read one row of a BF16 tensor directly, dequantizing in-place. |
| 637 | +/// `abs_offset` is the file offset of this row's BF16 data. |
| 638 | +fn read_bf16_row_f32<R: Read + Seek>( |
| 639 | + reader: &mut R, |
| 640 | + abs_offset: u64, |
| 641 | + n_cols: usize, |
| 642 | + buf: &mut Vec<u8>, |
| 643 | + row_f32: &mut Vec<f32>, |
| 644 | +) -> Result<(), String> { |
| 645 | + let row_bytes = n_cols * 2; |
| 646 | + buf.resize(row_bytes, 0); |
| 647 | + row_f32.resize(n_cols, 0.0); |
| 648 | + |
| 649 | + reader.seek(SeekFrom::Start(abs_offset)).map_err(|e| e.to_string())?; |
| 650 | + reader.read_exact(&mut buf[..row_bytes]).map_err(|e| e.to_string())?; |
| 651 | + |
| 652 | + // SAFETY: BF16 is #[repr(transparent)] over u16, same layout as [u8; 2] LE pairs. |
| 653 | + let bf16_slice: &[super::quantized::BF16] = unsafe { |
| 654 | + std::slice::from_raw_parts(buf.as_ptr() as *const super::quantized::BF16, n_cols) |
| 655 | + }; |
| 656 | + super::quantized::bf16_to_f32_slice(bf16_slice, &mut row_f32[..n_cols]); |
| 657 | + Ok(()) |
| 658 | +} |
| 659 | + |
| 660 | +/// Read one row of an F16 tensor directly, dequantizing in-place. |
| 661 | +fn read_f16_row_f32<R: Read + Seek>( |
| 662 | + reader: &mut R, |
| 663 | + abs_offset: u64, |
| 664 | + n_cols: usize, |
| 665 | + buf: &mut Vec<u8>, |
| 666 | + row_f32: &mut Vec<f32>, |
| 667 | +) -> Result<(), String> { |
| 668 | + let row_bytes = n_cols * 2; |
| 669 | + buf.resize(row_bytes, 0); |
| 670 | + row_f32.resize(n_cols, 0.0); |
| 671 | + |
| 672 | + reader.seek(SeekFrom::Start(abs_offset)).map_err(|e| e.to_string())?; |
| 673 | + reader.read_exact(&mut buf[..row_bytes]).map_err(|e| e.to_string())?; |
| 674 | + |
| 675 | + for (i, c) in buf[..row_bytes].chunks_exact(2).enumerate() { |
| 676 | + let bits = u16::from_le_bytes([c[0], c[1]]); |
| 677 | + row_f32[i] = gguf::f16_to_f32(bits); |
| 678 | + } |
| 679 | + Ok(()) |
| 680 | +} |
| 681 | + |
| 682 | +/// Read one row of an F32 tensor directly. |
| 683 | +fn read_f32_row<R: Read + Seek>( |
| 684 | + reader: &mut R, |
| 685 | + abs_offset: u64, |
| 686 | + n_cols: usize, |
| 687 | + buf: &mut Vec<u8>, |
| 688 | + row_f32: &mut Vec<f32>, |
| 689 | +) -> Result<(), String> { |
| 690 | + let row_bytes = n_cols * 4; |
| 691 | + buf.resize(row_bytes, 0); |
| 692 | + row_f32.resize(n_cols, 0.0); |
| 693 | + |
| 694 | + reader.seek(SeekFrom::Start(abs_offset)).map_err(|e| e.to_string())?; |
| 695 | + reader.read_exact(&mut buf[..row_bytes]).map_err(|e| e.to_string())?; |
| 696 | + |
| 697 | + for (i, c) in buf[..row_bytes].chunks_exact(4).enumerate() { |
| 698 | + row_f32[i] = f32::from_le_bytes([c[0], c[1], c[2], c[3]]); |
| 699 | + } |
| 700 | + Ok(()) |
| 701 | +} |
| 702 | + |
| 703 | +/// Stream-index a GGUF file with row-wise streaming for large tensors. |
| 704 | +/// |
| 705 | +/// Identical to `stream_index_gguf` for tensors under `LARGE_TENSOR_THRESHOLD`, |
| 706 | +/// but processes oversized tensors (e.g. Maverick's 20 GB embeddings) one row |
| 707 | +/// at a time — peak RAM per large tensor = one row (~20 KB–55 KB) instead of |
| 708 | +/// the full tensor. |
| 709 | +/// |
| 710 | +/// Supports row-wise streaming for F32, F16, and BF16 dtypes. |
| 711 | +/// Quantized large tensors are skipped (rare — quantized blocks don't align to rows). |
| 712 | +pub fn stream_index_gguf_large<R: Read + Seek, W: Write>( |
| 713 | + reader: &mut R, |
| 714 | + writer: &mut W, |
| 715 | + callback: Option<&dyn Fn(&str, &LayerType, usize, usize)>, |
| 716 | +) -> Result<IndexStats, String> { |
| 717 | + let gguf = gguf::read_gguf_header(reader)?; |
| 718 | + let mut stats = IndexStats::default(); |
| 719 | + stats.tensors_total = gguf.tensors.len(); |
| 720 | + |
| 721 | + // Write file header: magic + tensor count |
| 722 | + writer.write_all(b"BGZ7").map_err(|e| e.to_string())?; |
| 723 | + writer.write_all(&(gguf.tensors.len() as u32).to_le_bytes()).map_err(|e| e.to_string())?; |
| 724 | + |
| 725 | + // Reusable row buffers for large-tensor streaming |
| 726 | + let mut row_buf: Vec<u8> = Vec::new(); |
| 727 | + let mut row_f32: Vec<f32> = Vec::new(); |
| 728 | + |
| 729 | + for tensor in &gguf.tensors { |
| 730 | + let layer_type = classify_tensor(&tensor.name, &tensor.dimensions); |
| 731 | + |
| 732 | + // Skip norms and tiny tensors |
| 733 | + if matches!(layer_type, LayerType::Skip | LayerType::Norm) { |
| 734 | + stats.tensors_skipped += 1; |
| 735 | + continue; |
| 736 | + } |
| 737 | + |
| 738 | + let n_elements = tensor.element_count() as usize; |
| 739 | + let is_large = n_elements > LARGE_TENSOR_THRESHOLD; |
| 740 | + |
| 741 | + if is_large { |
| 742 | + // ── Row-wise streaming path for large tensors ── |
| 743 | + // Only supported for unquantized types where rows align to file offsets. |
| 744 | + let elem_size = match tensor.dtype { |
| 745 | + GgmlType::BF16 => 2usize, |
| 746 | + GgmlType::F16 => 2, |
| 747 | + GgmlType::F32 => 4, |
| 748 | + _ => { |
| 749 | + // Quantized large tensors: skip (block structure doesn't align to rows) |
| 750 | + eprintln!(" SKIP large quantized tensor: {} ({:?}, {} elements)", |
| 751 | + tensor.name, tensor.dtype, n_elements); |
| 752 | + stats.tensors_skipped += 1; |
| 753 | + continue; |
| 754 | + } |
| 755 | + }; |
| 756 | + |
| 757 | + // Determine rows × cols |
| 758 | + let (n_rows, n_cols) = if tensor.dimensions.len() >= 2 { |
| 759 | + let rows = tensor.dimensions[0] as usize; |
| 760 | + let cols: usize = tensor.dimensions[1..].iter().map(|&d| d as usize).product(); |
| 761 | + (rows, cols) |
| 762 | + } else { |
| 763 | + (1, n_elements) |
| 764 | + }; |
| 765 | + |
| 766 | + let tensor_f32_bytes = (n_rows as u64) * (n_cols as u64) * 4; |
| 767 | + if tensor_f32_bytes > stats.peak_tensor_bytes { |
| 768 | + // Record the logical size, even though we never allocate it all |
| 769 | + stats.peak_tensor_bytes = tensor_f32_bytes; |
| 770 | + } |
| 771 | + |
| 772 | + let abs_base = gguf.tensor_data_offset + tensor.offset; |
| 773 | + |
| 774 | + // Project each row one at a time |
| 775 | + let mut rows = Vec::with_capacity(n_rows); |
| 776 | + for r in 0..n_rows { |
| 777 | + let row_offset = abs_base + (r as u64) * (n_cols as u64) * (elem_size as u64); |
| 778 | + match tensor.dtype { |
| 779 | + GgmlType::BF16 => read_bf16_row_f32(reader, row_offset, n_cols, &mut row_buf, &mut row_f32)?, |
| 780 | + GgmlType::F16 => read_f16_row_f32(reader, row_offset, n_cols, &mut row_buf, &mut row_f32)?, |
| 781 | + GgmlType::F32 => read_f32_row(reader, row_offset, n_cols, &mut row_buf, &mut row_f32)?, |
| 782 | + _ => unreachable!(), // guarded above |
| 783 | + }; |
| 784 | + rows.push(project_row_to_base17(&row_f32[..n_cols])); |
| 785 | + } |
| 786 | + |
| 787 | + let ct = CompressedTensor { |
| 788 | + name: tensor.name.clone(), |
| 789 | + layer_type: layer_type.clone(), |
| 790 | + original_shape: tensor.dimensions.clone(), |
| 791 | + n_rows, |
| 792 | + n_cols, |
| 793 | + rows, |
| 794 | + }; |
| 795 | + |
| 796 | + let orig = ct.original_bytes() as u64; |
| 797 | + let comp = ct.compressed_bytes() as u64; |
| 798 | + stats.tensors_indexed += 1; |
| 799 | + stats.original_bytes += orig; |
| 800 | + stats.compressed_bytes += comp; |
| 801 | + |
| 802 | + let lt_idx = match &ct.layer_type { |
| 803 | + LayerType::Attention => 0, |
| 804 | + LayerType::FeedForward => 1, |
| 805 | + LayerType::Conv2D => 2, |
| 806 | + LayerType::Norm => 3, |
| 807 | + LayerType::Embedding => 4, |
| 808 | + LayerType::Skip => 5, |
| 809 | + }; |
| 810 | + stats.by_type[lt_idx].0 += 1; |
| 811 | + stats.by_type[lt_idx].1 += orig; |
| 812 | + stats.by_type[lt_idx].2 += comp; |
| 813 | + |
| 814 | + if let Some(cb) = callback { |
| 815 | + cb(&ct.name, &ct.layer_type, ct.original_bytes(), ct.compressed_bytes()); |
| 816 | + } |
| 817 | + |
| 818 | + ct.write_to(writer)?; |
| 819 | + } else { |
| 820 | + // ── Standard path: load full tensor (same as stream_index_gguf) ── |
| 821 | + let data = gguf::read_tensor_f32(reader, &gguf, tensor)?; |
| 822 | + |
| 823 | + let tensor_bytes = data.len() as u64 * 4; |
| 824 | + if tensor_bytes > stats.peak_tensor_bytes { |
| 825 | + stats.peak_tensor_bytes = tensor_bytes; |
| 826 | + } |
| 827 | + |
| 828 | + let (n_rows, n_cols) = tensor_to_rows(&data, &tensor.dimensions, &layer_type); |
| 829 | + |
| 830 | + let mut rows = Vec::with_capacity(n_rows); |
| 831 | + for r in 0..n_rows { |
| 832 | + let start = r * n_cols; |
| 833 | + let end = (start + n_cols).min(data.len()); |
| 834 | + rows.push(project_row_to_base17(&data[start..end])); |
| 835 | + } |
| 836 | + |
| 837 | + let ct = CompressedTensor { |
| 838 | + name: tensor.name.clone(), |
| 839 | + layer_type: layer_type.clone(), |
| 840 | + original_shape: tensor.dimensions.clone(), |
| 841 | + n_rows, |
| 842 | + n_cols, |
| 843 | + rows, |
| 844 | + }; |
| 845 | + |
| 846 | + let orig = ct.original_bytes() as u64; |
| 847 | + let comp = ct.compressed_bytes() as u64; |
| 848 | + stats.tensors_indexed += 1; |
| 849 | + stats.original_bytes += orig; |
| 850 | + stats.compressed_bytes += comp; |
| 851 | + |
| 852 | + let lt_idx = match &ct.layer_type { |
| 853 | + LayerType::Attention => 0, |
| 854 | + LayerType::FeedForward => 1, |
| 855 | + LayerType::Conv2D => 2, |
| 856 | + LayerType::Norm => 3, |
| 857 | + LayerType::Embedding => 4, |
| 858 | + LayerType::Skip => 5, |
| 859 | + }; |
| 860 | + stats.by_type[lt_idx].0 += 1; |
| 861 | + stats.by_type[lt_idx].1 += orig; |
| 862 | + stats.by_type[lt_idx].2 += comp; |
| 863 | + |
| 864 | + if let Some(cb) = callback { |
| 865 | + cb(&ct.name, &ct.layer_type, ct.original_bytes(), ct.compressed_bytes()); |
| 866 | + } |
| 867 | + |
| 868 | + ct.write_to(writer)?; |
| 869 | + } |
| 870 | + } |
| 871 | + |
| 872 | + Ok(stats) |
| 873 | +} |
| 874 | + |
633 | 875 | // ============================================================================ |
634 | 876 | // Tests |
635 | 877 | // ============================================================================ |
@@ -1114,7 +1356,7 @@ mod tests { |
1114 | 1356 | let out = std::fs::File::create(&out_path).expect("create output"); |
1115 | 1357 | let mut writer = BufWriter::new(out); |
1116 | 1358 |
|
1117 | | - let stats = stream_index_gguf( |
| 1359 | + let stats = stream_index_gguf_large( |
1118 | 1360 | &mut reader, |
1119 | 1361 | &mut writer, |
1120 | 1362 | Some(&|name, layer_type, orig, comp| { |
|
0 commit comments