mirror of
https://github.com/anchore/syft.git
synced 2026-10-30 14:02:26 +01:00
Pin Gguff lib to prevent dropping packages (#5447)
* test: catalog GGUF models larger than the header gguf-parser-go v0.26.3+ rejects a header whose tensors declare more data than follows it. Syft parses only a 50 MB copy of each model, so any larger model is dropped. Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> * fix: fix parser with rollback and verify with test Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> * fix: bound GGUF tensor info before the parser reads it The header pre-scan stopped after the metadata KVs and left tensor infos to gguf-parser-go. Older releases of the library allocate whatever length a tensor name declares, so a tiny file could force a multi-GB allocation, or a fatal out of memory that panic recovery can't catch. - tensor names are bounded like metadata keys (non-empty, length limit, shared string budget) - dimension count is capped at `GGML_MAX_DIMS` - a tensor count larger than the header now fails at EOF in the pre-scan Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix: give the GGUF header copy the model's real size The cataloger copies at most 50 MB of a model and parses that copy. Newer gguf-parser-go releases check the tensor data the header declares against the file size, so the truncated copy of any larger model was rejected. - after the header is validated, the copy is extended to the size the resolver reports for the model, leaving the tail as a hole the parser never reads - sources that report no size (OCI model artifacts today) keep the old behavior Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * chore(deps): restore gguf-parser-go v0.26.4 The header copy now carries the model's real size, so the pin to v0.26.0 and its dependabot ignore are no longer needed. This also brings back upstream's string and array length bounds, and the library's own `IsNumeric`. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix: keep the extended GGUF header copy sparse on Windows NTFS reserves disk for the full length of a file extended with truncate unless the file is marked sparse, so cataloging a large model on Windows reserved the model's size in the temp dir until cleanup. - the header copy is marked sparse (`FSCTL_SET_SPARSE`) before it is extended - if the filesystem can't make it sparse (FAT32, exFAT), the copy is not extended, and models larger than it fail to parse as before - `golang.org/x/sys` moves from an indirect to a direct dependency Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
co-authored by
Alex Goodman
parent
a81193226f
commit
f15c3343e2
@@ -0,0 +1,126 @@
|
||||
// generate writes header.gguf: the header of a GGUF model shaped like Qwen2.5-0.5B-Instruct at Q2_K (290
|
||||
// tensors declaring about 175 MB of data), padded to the tensor data alignment. Tests extend a copy to a
|
||||
// model's size as a sparse file, which stands in for the full model without storing its data.
|
||||
//
|
||||
// This file lives under testdata/ so the Go build system ignores it. Run it when changing the fixture:
|
||||
//
|
||||
// go run ./testdata/models/gguf-header-only/generate.go ./testdata/models/gguf-header-only/header.gguf
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
typeUint32 = 4
|
||||
typeString = 8
|
||||
typeArray = 9
|
||||
|
||||
ggmlF32 = 0
|
||||
ggmlQ2K = 10
|
||||
ggmlQ3K = 11
|
||||
ggmlQ4K = 12
|
||||
fileQ2K = 10
|
||||
)
|
||||
|
||||
type tensor struct {
|
||||
name string
|
||||
typ uint32
|
||||
dims []uint64
|
||||
}
|
||||
|
||||
type writer struct{ bytes.Buffer }
|
||||
|
||||
func (w *writer) le(v any) { _ = binary.Write(w, binary.LittleEndian, v) }
|
||||
|
||||
func (w *writer) str(s string) {
|
||||
w.le(uint64(len(s)))
|
||||
w.WriteString(s)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Fprintln(os.Stderr, "usage: generate <output path>")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
const (
|
||||
hidden = 896
|
||||
kvDim = 128
|
||||
ffn = 4864
|
||||
vocab = 151936
|
||||
layers = 24
|
||||
)
|
||||
tensors := []tensor{{"token_embd.weight", ggmlQ4K, []uint64{hidden, vocab}}}
|
||||
for i := range layers {
|
||||
blk := func(n string) string { return fmt.Sprintf("blk.%d.%s", i, n) }
|
||||
tensors = append(tensors,
|
||||
tensor{blk("attn_norm.weight"), ggmlF32, []uint64{hidden}},
|
||||
tensor{blk("attn_q.weight"), ggmlQ2K, []uint64{hidden, hidden}},
|
||||
tensor{blk("attn_q.bias"), ggmlF32, []uint64{hidden}},
|
||||
tensor{blk("attn_k.weight"), ggmlQ2K, []uint64{hidden, kvDim}},
|
||||
tensor{blk("attn_k.bias"), ggmlF32, []uint64{kvDim}},
|
||||
tensor{blk("attn_v.weight"), ggmlQ3K, []uint64{hidden, kvDim}},
|
||||
tensor{blk("attn_v.bias"), ggmlF32, []uint64{kvDim}},
|
||||
tensor{blk("attn_output.weight"), ggmlQ3K, []uint64{hidden, hidden}},
|
||||
tensor{blk("ffn_norm.weight"), ggmlF32, []uint64{hidden}},
|
||||
tensor{blk("ffn_gate.weight"), ggmlQ2K, []uint64{hidden, ffn}},
|
||||
tensor{blk("ffn_up.weight"), ggmlQ2K, []uint64{hidden, ffn}},
|
||||
tensor{blk("ffn_down.weight"), ggmlQ3K, []uint64{ffn, hidden}},
|
||||
)
|
||||
}
|
||||
tensors = append(tensors, tensor{"output_norm.weight", ggmlF32, []uint64{hidden}})
|
||||
|
||||
var w writer
|
||||
w.le(uint32(0x46554747)) // "GGUF"
|
||||
w.le(uint32(3))
|
||||
w.le(uint64(len(tensors)))
|
||||
w.le(uint64(6)) // metadata KV count
|
||||
|
||||
w.str("general.architecture")
|
||||
w.le(uint32(typeString))
|
||||
w.str("qwen2")
|
||||
w.str("general.name")
|
||||
w.le(uint32(typeString))
|
||||
w.str("qwen2.5-0.5b-instruct")
|
||||
w.str("general.license")
|
||||
w.le(uint32(typeString))
|
||||
w.str("apache-2.0")
|
||||
w.str("general.file_type")
|
||||
w.le(uint32(typeUint32))
|
||||
w.le(uint32(fileQ2K))
|
||||
w.str("qwen2.block_count")
|
||||
w.le(uint32(typeUint32))
|
||||
w.le(uint32(layers))
|
||||
// a vocabulary the parser skips over
|
||||
w.str("tokenizer.ggml.tokens")
|
||||
w.le(uint32(typeArray))
|
||||
w.le(uint32(typeString))
|
||||
w.le(uint64(3))
|
||||
for _, tok := range []string{"!", "\"", "#"} {
|
||||
w.str(tok)
|
||||
}
|
||||
|
||||
var offset uint64
|
||||
for _, t := range tensors {
|
||||
w.str(t.name)
|
||||
w.le(uint32(len(t.dims)))
|
||||
for _, d := range t.dims {
|
||||
w.le(d)
|
||||
}
|
||||
w.le(t.typ)
|
||||
w.le(offset)
|
||||
offset += 1 << 20 // not used by syft; real offsets depend on each type's block size
|
||||
}
|
||||
|
||||
// pad to the default 32-byte alignment, where the tensor data starts
|
||||
w.Write(make([]byte, (32-w.Len()%32)%32))
|
||||
|
||||
if err := os.WriteFile(os.Args[1], w.Bytes(), 0o644); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Reference in New Issue
Block a user