569 lines
15 KiB
Go
569 lines
15 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
GGUFMagic uint32 = 0x46554747
|
|
DefaultAlignment uint64 = 32
|
|
)
|
|
|
|
type ValueType uint32
|
|
|
|
const (
|
|
TypeUint8 ValueType = 0
|
|
TypeInt8 ValueType = 1
|
|
TypeUint16 ValueType = 2
|
|
TypeInt16 ValueType = 3
|
|
TypeUint32 ValueType = 4
|
|
TypeInt32 ValueType = 5
|
|
TypeFloat32 ValueType = 6
|
|
TypeBool ValueType = 7
|
|
TypeString ValueType = 8
|
|
TypeArray ValueType = 9
|
|
TypeUint64 ValueType = 10
|
|
TypeInt64 ValueType = 11
|
|
TypeFloat64 ValueType = 12
|
|
)
|
|
|
|
var typeNames = map[ValueType]string{
|
|
TypeUint8: "uint8", TypeInt8: "int8",
|
|
TypeUint16: "uint16", TypeInt16: "int16",
|
|
TypeUint32: "uint32", TypeInt32: "int32",
|
|
TypeFloat32: "float32", TypeBool: "bool",
|
|
TypeString: "string", TypeArray: "array",
|
|
TypeUint64: "uint64", TypeInt64: "int64",
|
|
TypeFloat64: "float64",
|
|
}
|
|
|
|
// ScalarTypes returns all non-array value types for the type picker.
|
|
var ScalarTypes = []ValueType{
|
|
TypeString, TypeBool,
|
|
TypeUint8, TypeInt8, TypeUint16, TypeInt16,
|
|
TypeUint32, TypeInt32, TypeUint64, TypeInt64,
|
|
TypeFloat32, TypeFloat64,
|
|
}
|
|
|
|
func (t ValueType) String() string {
|
|
if s, ok := typeNames[t]; ok {
|
|
return s
|
|
}
|
|
return fmt.Sprintf("unknown(%d)", t)
|
|
}
|
|
|
|
// MetaValue holds a single GGUF metadata value.
|
|
type MetaValue struct {
|
|
Type ValueType
|
|
Data interface{}
|
|
ElemType ValueType // only for arrays
|
|
}
|
|
|
|
// DisplayString returns a human-readable representation of the value.
|
|
func (v MetaValue) DisplayString() string {
|
|
switch v.Type {
|
|
case TypeBool:
|
|
if v.Data.(bool) {
|
|
return "true"
|
|
}
|
|
return "false"
|
|
case TypeString:
|
|
return v.Data.(string)
|
|
case TypeFloat32:
|
|
return strconv.FormatFloat(float64(v.Data.(float32)), 'g', -1, 32)
|
|
case TypeFloat64:
|
|
return strconv.FormatFloat(v.Data.(float64), 'g', -1, 64)
|
|
case TypeArray:
|
|
arr := v.Data.([]MetaValue)
|
|
return fmt.Sprintf("[%s x %d]", v.ElemType, len(arr))
|
|
default:
|
|
return fmt.Sprintf("%v", v.Data)
|
|
}
|
|
}
|
|
|
|
// TypeName returns the display name for this value's type.
|
|
func (v MetaValue) TypeName() string {
|
|
if v.Type == TypeArray {
|
|
return fmt.Sprintf("array[%s]", v.ElemType)
|
|
}
|
|
return v.Type.String()
|
|
}
|
|
|
|
// KVPair is a metadata key-value pair.
|
|
type KVPair struct {
|
|
Key string
|
|
Value MetaValue
|
|
}
|
|
|
|
// TensorInfo describes a single tensor in the file.
|
|
type TensorInfo struct {
|
|
Name string
|
|
NDims uint32
|
|
Dims []uint64
|
|
DType uint32
|
|
Offset uint64
|
|
}
|
|
|
|
// GGUFFile represents a parsed GGUF file's header and metadata.
|
|
// Tensor data is never loaded into memory.
|
|
type GGUFFile struct {
|
|
Version uint32
|
|
TensorCount uint64
|
|
Metadata []KVPair
|
|
Tensors []TensorInfo
|
|
Alignment uint64
|
|
|
|
sourcePath string
|
|
tensorDataStart int64
|
|
fileSize int64
|
|
}
|
|
|
|
// ParseGGUF reads only the header, metadata, and tensor info from a GGUF file.
|
|
// No tensor data is loaded into memory.
|
|
func ParseGGUF(path string) (*GGUFFile, error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
|
|
info, err := f.Stat()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
g := &GGUFFile{
|
|
sourcePath: path,
|
|
fileSize: info.Size(),
|
|
Alignment: DefaultAlignment,
|
|
}
|
|
|
|
var magic uint32
|
|
if err := binary.Read(f, binary.LittleEndian, &magic); err != nil {
|
|
return nil, fmt.Errorf("reading magic: %w", err)
|
|
}
|
|
if magic != GGUFMagic {
|
|
return nil, fmt.Errorf("not a GGUF file (magic: 0x%08X, expected 0x%08X)", magic, GGUFMagic)
|
|
}
|
|
|
|
if err := binary.Read(f, binary.LittleEndian, &g.Version); err != nil {
|
|
return nil, fmt.Errorf("reading version: %w", err)
|
|
}
|
|
if g.Version < 2 || g.Version > 3 {
|
|
return nil, fmt.Errorf("unsupported GGUF version %d (supported: 2, 3)", g.Version)
|
|
}
|
|
|
|
if err := binary.Read(f, binary.LittleEndian, &g.TensorCount); err != nil {
|
|
return nil, fmt.Errorf("reading tensor count: %w", err)
|
|
}
|
|
var metaCount uint64
|
|
if err := binary.Read(f, binary.LittleEndian, &metaCount); err != nil {
|
|
return nil, fmt.Errorf("reading metadata count: %w", err)
|
|
}
|
|
|
|
// Read metadata KV pairs
|
|
g.Metadata = make([]KVPair, 0, metaCount)
|
|
for i := uint64(0); i < metaCount; i++ {
|
|
key, err := readString(f)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("metadata[%d] key: %w", i, err)
|
|
}
|
|
var vtype uint32
|
|
if err := binary.Read(f, binary.LittleEndian, &vtype); err != nil {
|
|
return nil, fmt.Errorf("metadata[%d] type: %w", i, err)
|
|
}
|
|
val, err := readValue(f, ValueType(vtype))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("metadata[%d] (%s) value: %w", i, key, err)
|
|
}
|
|
g.Metadata = append(g.Metadata, KVPair{Key: key, Value: val})
|
|
|
|
if key == "general.alignment" {
|
|
switch val.Type {
|
|
case TypeUint32:
|
|
g.Alignment = uint64(val.Data.(uint32))
|
|
case TypeUint64:
|
|
g.Alignment = val.Data.(uint64)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Read tensor infos
|
|
g.Tensors = make([]TensorInfo, 0, g.TensorCount)
|
|
for i := uint64(0); i < g.TensorCount; i++ {
|
|
name, err := readString(f)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("tensor[%d] name: %w", i, err)
|
|
}
|
|
var ndims uint32
|
|
if err := binary.Read(f, binary.LittleEndian, &ndims); err != nil {
|
|
return nil, fmt.Errorf("tensor[%d] ndims: %w", i, err)
|
|
}
|
|
dims := make([]uint64, ndims)
|
|
for d := uint32(0); d < ndims; d++ {
|
|
if err := binary.Read(f, binary.LittleEndian, &dims[d]); err != nil {
|
|
return nil, fmt.Errorf("tensor[%d] dim[%d]: %w", i, d, err)
|
|
}
|
|
}
|
|
var dtype uint32
|
|
if err := binary.Read(f, binary.LittleEndian, &dtype); err != nil {
|
|
return nil, fmt.Errorf("tensor[%d] dtype: %w", i, err)
|
|
}
|
|
var offset uint64
|
|
if err := binary.Read(f, binary.LittleEndian, &offset); err != nil {
|
|
return nil, fmt.Errorf("tensor[%d] offset: %w", i, err)
|
|
}
|
|
g.Tensors = append(g.Tensors, TensorInfo{
|
|
Name: name, NDims: ndims, Dims: dims, DType: dtype, Offset: offset,
|
|
})
|
|
}
|
|
|
|
// Record where tensor data begins (aligned)
|
|
pos, err := f.Seek(0, io.SeekCurrent)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("getting position: %w", err)
|
|
}
|
|
g.tensorDataStart = alignUp(pos, int64(g.Alignment))
|
|
|
|
return g, nil
|
|
}
|
|
|
|
// Save writes the GGUF file with (potentially modified) metadata to outPath.
|
|
// Tensor data is streamed from the original file — never fully loaded.
|
|
func (g *GGUFFile) Save(outPath string) error {
|
|
out, err := os.Create(outPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer out.Close()
|
|
|
|
cw := &countingWriter{w: out}
|
|
|
|
// Header
|
|
if err := binary.Write(cw, binary.LittleEndian, GGUFMagic); err != nil {
|
|
return fmt.Errorf("writing magic: %w", err)
|
|
}
|
|
if err := binary.Write(cw, binary.LittleEndian, g.Version); err != nil {
|
|
return fmt.Errorf("writing version: %w", err)
|
|
}
|
|
if err := binary.Write(cw, binary.LittleEndian, g.TensorCount); err != nil {
|
|
return fmt.Errorf("writing tensor count: %w", err)
|
|
}
|
|
if err := binary.Write(cw, binary.LittleEndian, uint64(len(g.Metadata))); err != nil {
|
|
return fmt.Errorf("writing metadata count: %w", err)
|
|
}
|
|
|
|
// Metadata KV pairs
|
|
for _, kv := range g.Metadata {
|
|
if err := writeString(cw, kv.Key); err != nil {
|
|
return fmt.Errorf("writing key %q: %w", kv.Key, err)
|
|
}
|
|
if err := binary.Write(cw, binary.LittleEndian, uint32(kv.Value.Type)); err != nil {
|
|
return fmt.Errorf("writing type for %q: %w", kv.Key, err)
|
|
}
|
|
if err := writeValue(cw, kv.Value); err != nil {
|
|
return fmt.Errorf("writing value for %q: %w", kv.Key, err)
|
|
}
|
|
}
|
|
|
|
// Tensor infos
|
|
for _, t := range g.Tensors {
|
|
if err := writeString(cw, t.Name); err != nil {
|
|
return fmt.Errorf("writing tensor name: %w", err)
|
|
}
|
|
if err := binary.Write(cw, binary.LittleEndian, t.NDims); err != nil {
|
|
return err
|
|
}
|
|
for _, d := range t.Dims {
|
|
if err := binary.Write(cw, binary.LittleEndian, d); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := binary.Write(cw, binary.LittleEndian, t.DType); err != nil {
|
|
return err
|
|
}
|
|
if err := binary.Write(cw, binary.LittleEndian, t.Offset); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Alignment padding
|
|
aligned := alignUp(cw.count, int64(g.Alignment))
|
|
padLen := aligned - cw.count
|
|
if padLen > 0 {
|
|
if _, err := out.Write(make([]byte, padLen)); err != nil {
|
|
return fmt.Errorf("writing padding: %w", err)
|
|
}
|
|
}
|
|
|
|
// Stream-copy tensor data from original file
|
|
src, err := os.Open(g.sourcePath)
|
|
if err != nil {
|
|
return fmt.Errorf("opening source for tensor data: %w", err)
|
|
}
|
|
defer src.Close()
|
|
|
|
if _, err := src.Seek(g.tensorDataStart, io.SeekStart); err != nil {
|
|
return fmt.Errorf("seeking to tensor data: %w", err)
|
|
}
|
|
|
|
tensorLen := g.fileSize - g.tensorDataStart
|
|
if tensorLen > 0 {
|
|
if _, err := io.CopyN(out, src, tensorLen); err != nil {
|
|
return fmt.Errorf("copying tensor data: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ParseValueFromString parses a user-entered string into a MetaValue of the given type.
|
|
func ParseValueFromString(s string, t ValueType) (MetaValue, error) {
|
|
s = strings.TrimSpace(s)
|
|
switch t {
|
|
case TypeUint8:
|
|
v, err := strconv.ParseUint(s, 10, 8)
|
|
return MetaValue{Type: t, Data: uint8(v)}, err
|
|
case TypeInt8:
|
|
v, err := strconv.ParseInt(s, 10, 8)
|
|
return MetaValue{Type: t, Data: int8(v)}, err
|
|
case TypeUint16:
|
|
v, err := strconv.ParseUint(s, 10, 16)
|
|
return MetaValue{Type: t, Data: uint16(v)}, err
|
|
case TypeInt16:
|
|
v, err := strconv.ParseInt(s, 10, 16)
|
|
return MetaValue{Type: t, Data: int16(v)}, err
|
|
case TypeUint32:
|
|
v, err := strconv.ParseUint(s, 10, 32)
|
|
return MetaValue{Type: t, Data: uint32(v)}, err
|
|
case TypeInt32:
|
|
v, err := strconv.ParseInt(s, 10, 32)
|
|
return MetaValue{Type: t, Data: int32(v)}, err
|
|
case TypeUint64:
|
|
v, err := strconv.ParseUint(s, 10, 64)
|
|
return MetaValue{Type: t, Data: v}, err
|
|
case TypeInt64:
|
|
v, err := strconv.ParseInt(s, 10, 64)
|
|
return MetaValue{Type: t, Data: v}, err
|
|
case TypeFloat32:
|
|
v, err := strconv.ParseFloat(s, 32)
|
|
return MetaValue{Type: t, Data: float32(v)}, err
|
|
case TypeFloat64:
|
|
v, err := strconv.ParseFloat(s, 64)
|
|
return MetaValue{Type: t, Data: v}, err
|
|
case TypeBool:
|
|
v, err := strconv.ParseBool(s)
|
|
return MetaValue{Type: t, Data: v}, err
|
|
case TypeString:
|
|
return MetaValue{Type: t, Data: s}, nil
|
|
default:
|
|
return MetaValue{}, fmt.Errorf("cannot parse type %s from string", t)
|
|
}
|
|
}
|
|
|
|
// --- internal helpers ---
|
|
|
|
func readString(r io.Reader) (string, error) {
|
|
var length uint64
|
|
if err := binary.Read(r, binary.LittleEndian, &length); err != nil {
|
|
return "", err
|
|
}
|
|
if length > 1<<30 { // sanity limit: 1 GB
|
|
return "", fmt.Errorf("string length %d exceeds sanity limit", length)
|
|
}
|
|
buf := make([]byte, length)
|
|
if _, err := io.ReadFull(r, buf); err != nil {
|
|
return "", err
|
|
}
|
|
return string(buf), nil
|
|
}
|
|
|
|
func readValue(r io.Reader, t ValueType) (MetaValue, error) {
|
|
switch t {
|
|
case TypeUint8:
|
|
var v uint8
|
|
err := binary.Read(r, binary.LittleEndian, &v)
|
|
return MetaValue{Type: t, Data: v}, err
|
|
case TypeInt8:
|
|
var v int8
|
|
err := binary.Read(r, binary.LittleEndian, &v)
|
|
return MetaValue{Type: t, Data: v}, err
|
|
case TypeUint16:
|
|
var v uint16
|
|
err := binary.Read(r, binary.LittleEndian, &v)
|
|
return MetaValue{Type: t, Data: v}, err
|
|
case TypeInt16:
|
|
var v int16
|
|
err := binary.Read(r, binary.LittleEndian, &v)
|
|
return MetaValue{Type: t, Data: v}, err
|
|
case TypeUint32:
|
|
var v uint32
|
|
err := binary.Read(r, binary.LittleEndian, &v)
|
|
return MetaValue{Type: t, Data: v}, err
|
|
case TypeInt32:
|
|
var v int32
|
|
err := binary.Read(r, binary.LittleEndian, &v)
|
|
return MetaValue{Type: t, Data: v}, err
|
|
case TypeFloat32:
|
|
var v float32
|
|
err := binary.Read(r, binary.LittleEndian, &v)
|
|
return MetaValue{Type: t, Data: v}, err
|
|
case TypeBool:
|
|
var v uint8
|
|
err := binary.Read(r, binary.LittleEndian, &v)
|
|
return MetaValue{Type: t, Data: v != 0}, err
|
|
case TypeString:
|
|
s, err := readString(r)
|
|
return MetaValue{Type: t, Data: s}, err
|
|
case TypeUint64:
|
|
var v uint64
|
|
err := binary.Read(r, binary.LittleEndian, &v)
|
|
return MetaValue{Type: t, Data: v}, err
|
|
case TypeInt64:
|
|
var v int64
|
|
err := binary.Read(r, binary.LittleEndian, &v)
|
|
return MetaValue{Type: t, Data: v}, err
|
|
case TypeFloat64:
|
|
var v float64
|
|
err := binary.Read(r, binary.LittleEndian, &v)
|
|
return MetaValue{Type: t, Data: v}, err
|
|
case TypeArray:
|
|
var elemType uint32
|
|
if err := binary.Read(r, binary.LittleEndian, &elemType); err != nil {
|
|
return MetaValue{}, fmt.Errorf("array elem type: %w", err)
|
|
}
|
|
var count uint64
|
|
if err := binary.Read(r, binary.LittleEndian, &count); err != nil {
|
|
return MetaValue{}, fmt.Errorf("array count: %w", err)
|
|
}
|
|
if count > 1<<30 {
|
|
return MetaValue{}, fmt.Errorf("array count %d exceeds sanity limit", count)
|
|
}
|
|
arr := make([]MetaValue, count)
|
|
for i := uint64(0); i < count; i++ {
|
|
v, err := readValue(r, ValueType(elemType))
|
|
if err != nil {
|
|
return MetaValue{}, fmt.Errorf("array[%d]: %w", i, err)
|
|
}
|
|
arr[i] = v
|
|
}
|
|
return MetaValue{Type: TypeArray, Data: arr, ElemType: ValueType(elemType)}, nil
|
|
default:
|
|
return MetaValue{}, fmt.Errorf("unknown value type: %d", t)
|
|
}
|
|
}
|
|
|
|
func writeString(w io.Writer, s string) error {
|
|
if err := binary.Write(w, binary.LittleEndian, uint64(len(s))); err != nil {
|
|
return err
|
|
}
|
|
_, err := w.Write([]byte(s))
|
|
return err
|
|
}
|
|
|
|
func writeValue(w io.Writer, v MetaValue) error {
|
|
switch v.Type {
|
|
case TypeUint8:
|
|
return binary.Write(w, binary.LittleEndian, v.Data.(uint8))
|
|
case TypeInt8:
|
|
return binary.Write(w, binary.LittleEndian, v.Data.(int8))
|
|
case TypeUint16:
|
|
return binary.Write(w, binary.LittleEndian, v.Data.(uint16))
|
|
case TypeInt16:
|
|
return binary.Write(w, binary.LittleEndian, v.Data.(int16))
|
|
case TypeUint32:
|
|
return binary.Write(w, binary.LittleEndian, v.Data.(uint32))
|
|
case TypeInt32:
|
|
return binary.Write(w, binary.LittleEndian, v.Data.(int32))
|
|
case TypeFloat32:
|
|
return binary.Write(w, binary.LittleEndian, v.Data.(float32))
|
|
case TypeBool:
|
|
b := uint8(0)
|
|
if v.Data.(bool) {
|
|
b = 1
|
|
}
|
|
return binary.Write(w, binary.LittleEndian, b)
|
|
case TypeString:
|
|
return writeString(w, v.Data.(string))
|
|
case TypeUint64:
|
|
return binary.Write(w, binary.LittleEndian, v.Data.(uint64))
|
|
case TypeInt64:
|
|
return binary.Write(w, binary.LittleEndian, v.Data.(int64))
|
|
case TypeFloat64:
|
|
return binary.Write(w, binary.LittleEndian, v.Data.(float64))
|
|
case TypeArray:
|
|
arr := v.Data.([]MetaValue)
|
|
if err := binary.Write(w, binary.LittleEndian, uint32(v.ElemType)); err != nil {
|
|
return err
|
|
}
|
|
if err := binary.Write(w, binary.LittleEndian, uint64(len(arr))); err != nil {
|
|
return err
|
|
}
|
|
for i, elem := range arr {
|
|
if err := writeValue(w, elem); err != nil {
|
|
return fmt.Errorf("array[%d]: %w", i, err)
|
|
}
|
|
}
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("unknown type: %d", v.Type)
|
|
}
|
|
}
|
|
|
|
func alignUp(pos, align int64) int64 {
|
|
return (pos + align - 1) & ^(align - 1)
|
|
}
|
|
|
|
type countingWriter struct {
|
|
w io.Writer
|
|
count int64
|
|
}
|
|
|
|
func (cw *countingWriter) Write(p []byte) (int, error) {
|
|
n, err := cw.w.Write(p)
|
|
cw.count += int64(n)
|
|
return n, err
|
|
}
|
|
|
|
// humanSize formats a byte count as a human-readable string.
|
|
func humanSize(b int64) string {
|
|
const unit = 1024
|
|
if b < unit {
|
|
return fmt.Sprintf("%d B", b)
|
|
}
|
|
div, exp := int64(unit), 0
|
|
for n := b / unit; n >= unit; n /= unit {
|
|
div *= unit
|
|
exp++
|
|
}
|
|
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
|
|
}
|
|
|
|
// ggufTypeName returns the GGUF tensor type name.
|
|
func ggufTypeName(t uint32) string {
|
|
names := map[uint32]string{
|
|
0: "F32", 1: "F16", 2: "Q4_0", 3: "Q4_1",
|
|
6: "Q5_0", 7: "Q5_1", 8: "Q8_0", 9: "Q8_1",
|
|
10: "Q2_K", 11: "Q3_K", 12: "Q4_K", 13: "Q5_K", 14: "Q6_K", 15: "Q8_K",
|
|
16: "IQ2_XXS", 17: "IQ2_XS", 18: "IQ3_XXS", 19: "IQ1_S", 20: "IQ4_NL",
|
|
21: "IQ3_S", 22: "IQ2_S", 23: "IQ4_XS",
|
|
24: "I8", 25: "I16", 26: "I32", 27: "I64",
|
|
28: "F64", 29: "IQ1_M",
|
|
}
|
|
if s, ok := names[t]; ok {
|
|
return s
|
|
}
|
|
return fmt.Sprintf("type_%d", t)
|
|
}
|
|
|
|
// NaN/Inf-safe float display
|
|
func init() {
|
|
_ = math.NaN()
|
|
}
|