254 lines
6.3 KiB
Go
254 lines
6.3 KiB
Go
package main
|
|
|
|
// verify.go — prove the emitted GGUF equals the reference: identical metadata
|
|
// (keys, types, values; verified on raw wire bytes) and identical tensor set,
|
|
// shapes, ggml types and per-tensor payload bytes.
|
|
|
|
import (
|
|
"bytes"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
|
|
gguf "github.com/cymertek/go-gguf"
|
|
)
|
|
|
|
type verifyResult struct {
|
|
MetadataChecked int
|
|
TensorsChecked int
|
|
ByteEqual int
|
|
MetaMismatches []string
|
|
TensorMismatch []string
|
|
OrderDiffers bool
|
|
}
|
|
|
|
func (r *verifyResult) OK() bool {
|
|
return len(r.MetaMismatches) == 0 && len(r.TensorMismatch) == 0 && !r.OrderDiffers
|
|
}
|
|
|
|
func compareGGUF(refPath, gotPath string) (*verifyResult, error) {
|
|
res := &verifyResult{}
|
|
|
|
// --- metadata on raw wire bytes ---
|
|
_, refKVs, err := readRawKV(refPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read ref kv: %w", err)
|
|
}
|
|
_, gotKVs, err := readRawKV(gotPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read got kv: %w", err)
|
|
}
|
|
refMap := map[string]rawKV{}
|
|
for _, kv := range refKVs {
|
|
refMap[kv.Key] = kv
|
|
}
|
|
gotMap := map[string]rawKV{}
|
|
for _, kv := range gotKVs {
|
|
gotMap[kv.Key] = kv
|
|
}
|
|
for i, rk := range refKVs {
|
|
gk, ok := gotMap[rk.Key]
|
|
if !ok {
|
|
res.MetaMismatches = append(res.MetaMismatches, fmt.Sprintf("missing key %q", rk.Key))
|
|
continue
|
|
}
|
|
if i < len(gotKVs) && gotKVs[i].Key != rk.Key {
|
|
res.OrderDiffers = true
|
|
}
|
|
switch {
|
|
case gk.BType != rk.BType:
|
|
res.MetaMismatches = append(res.MetaMismatches, fmt.Sprintf("%s: type %s != %s", rk.Key, gk.BType.name(), rk.BType.name()))
|
|
case gk.rawKVHash() != rk.rawKVHash():
|
|
res.MetaMismatches = append(res.MetaMismatches, fmt.Sprintf("%s: value bytes differ", rk.Key))
|
|
}
|
|
res.MetadataChecked++
|
|
}
|
|
for _, gk := range gotKVs {
|
|
if _, ok := refMap[gk.Key]; !ok {
|
|
res.MetaMismatches = append(res.MetaMismatches, fmt.Sprintf("extra key %q", gk.Key))
|
|
}
|
|
}
|
|
|
|
// --- tensor metadata ---
|
|
refT, err := loadTensorSet(refPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
gotT, err := loadTensorSet(gotPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(refT.order) != len(gotT.order) {
|
|
res.TensorMismatch = append(res.TensorMismatch, fmt.Sprintf("tensor count %d != %d", len(gotT.order), len(refT.order)))
|
|
}
|
|
for i, name := range refT.order {
|
|
if i < len(gotT.order) && gotT.order[i] != name {
|
|
res.OrderDiffers = true
|
|
}
|
|
ri := refT.byName[name]
|
|
gi, ok := gotT.byName[name]
|
|
if !ok {
|
|
res.TensorMismatch = append(res.TensorMismatch, fmt.Sprintf("missing tensor %s", name))
|
|
continue
|
|
}
|
|
if !shapesEqual(ri.Shape, gi.Shape) {
|
|
res.TensorMismatch = append(res.TensorMismatch, fmt.Sprintf("%s: shape %v != %v", name, gi.Shape, ri.Shape))
|
|
continue
|
|
}
|
|
if ri.GgmlType != gi.GgmlType {
|
|
res.TensorMismatch = append(res.TensorMismatch, fmt.Sprintf("%s: type %s != %s", name, gi.GgmlType.GgmlName(), ri.GgmlType.GgmlName()))
|
|
continue
|
|
}
|
|
res.TensorsChecked++
|
|
}
|
|
for _, name := range gotT.order {
|
|
if _, ok := refT.byName[name]; !ok {
|
|
res.TensorMismatch = append(res.TensorMismatch, fmt.Sprintf("extra tensor %s", name))
|
|
}
|
|
}
|
|
|
|
// --- per-tensor payload bytes ---
|
|
refG, err := gguf.Open(refPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer refG.Close()
|
|
gotG, err := gguf.Open(gotPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer gotG.Close()
|
|
|
|
refHandles := map[string]*gguf.Tensor{}
|
|
for _, t := range mustTensors(refG) {
|
|
refHandles[t.Info().Name] = t
|
|
}
|
|
gotHandles := map[string]*gguf.Tensor{}
|
|
for _, t := range mustTensors(gotG) {
|
|
gotHandles[t.Info().Name] = t
|
|
}
|
|
for _, name := range refT.order {
|
|
rh, ok := refHandles[name]
|
|
if !ok {
|
|
continue
|
|
}
|
|
gh, ok := gotHandles[name]
|
|
if !ok {
|
|
continue
|
|
}
|
|
eq, diffOff, err := streamsEqual(rh.Reader(), gh.Reader())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("compare %s: %w", name, err)
|
|
}
|
|
if !eq {
|
|
res.TensorMismatch = append(res.TensorMismatch, fmt.Sprintf("%s: payload differs at byte %d", name, diffOff))
|
|
continue
|
|
}
|
|
res.ByteEqual++
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// tensorSet is an ordered view of a GGUF's tensor metadata.
|
|
type tensorSet struct {
|
|
order []string
|
|
byName map[string]gguf.TensorInfo
|
|
}
|
|
|
|
func loadTensorSet(path string) (*tensorSet, error) {
|
|
g, err := gguf.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer g.Close()
|
|
ts, err := g.Tensors()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := &tensorSet{byName: map[string]gguf.TensorInfo{}}
|
|
for _, t := range ts {
|
|
info := t.Info()
|
|
out.byName[info.Name] = info
|
|
out.order = append(out.order, info.Name)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func mustTensors(g *gguf.GGUF) []*gguf.Tensor {
|
|
ts, err := g.Tensors()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return ts
|
|
}
|
|
|
|
func shapesEqual(a, b []uint64) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// streamsEqual compares two readers byte-by-byte, returning the offset of the
|
|
// first difference.
|
|
func streamsEqual(a, b io.Reader) (bool, int64, error) {
|
|
const chunk = 1 << 20
|
|
ba := make([]byte, chunk)
|
|
bb := make([]byte, chunk)
|
|
var off int64
|
|
for {
|
|
na, ea := io.ReadFull(a, ba)
|
|
nb, eb := io.ReadFull(b, bb)
|
|
if na != nb {
|
|
return false, off + int64(min(na, nb)), nil
|
|
}
|
|
if !bytes.Equal(ba[:na], bb[:nb]) {
|
|
for i := 0; i < na; i++ {
|
|
if ba[i] != bb[i] {
|
|
return false, off + int64(i), nil
|
|
}
|
|
}
|
|
}
|
|
off += int64(na)
|
|
if ea == io.EOF || ea == io.ErrUnexpectedEOF {
|
|
endB := eb == io.EOF || eb == io.ErrUnexpectedEOF
|
|
return endB, off, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func cmdVerify(args []string) error {
|
|
fs := flag.NewFlagSet("verify", flag.ExitOnError)
|
|
ref := fs.String("ref", "", "reference GGUF")
|
|
got := fs.String("got", "", "candidate GGUF")
|
|
fs.Parse(args)
|
|
if *ref == "" || *got == "" {
|
|
return fmt.Errorf("verify: -ref and -got are required")
|
|
}
|
|
res, err := compareGGUF(*ref, *got)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("metadata: %d checked, %d mismatches\n", res.MetadataChecked, len(res.MetaMismatches))
|
|
fmt.Printf("tensors: %d checked, %d byte-identical, %d mismatches\n", res.TensorsChecked, res.ByteEqual, len(res.TensorMismatch))
|
|
if res.OrderDiffers {
|
|
fmt.Println("WARNING: metadata or tensor order differs from reference")
|
|
}
|
|
for _, m := range res.MetaMismatches {
|
|
fmt.Println(" META ", m)
|
|
}
|
|
for _, m := range res.TensorMismatch {
|
|
fmt.Println(" TENSOR", m)
|
|
}
|
|
if !res.OK() {
|
|
return fmt.Errorf("verification FAILED")
|
|
}
|
|
fmt.Printf("verification PASSED: %d metadata keys and %d tensors byte-identical\n", res.MetadataChecked, res.ByteEqual)
|
|
return nil
|
|
}
|