2024-05-21 18:28:16 +00:00
|
|
|
package convert
|
|
|
|
|
|
|
|
import (
|
2024-08-23 18:29:56 +00:00
|
|
|
"bytes"
|
2024-06-03 16:49:13 +00:00
|
|
|
"crypto/sha256"
|
2024-08-23 18:29:56 +00:00
|
|
|
"encoding/binary"
|
2024-08-01 21:52:15 +00:00
|
|
|
"encoding/hex"
|
2024-06-03 16:49:13 +00:00
|
|
|
"encoding/json"
|
|
|
|
"flag"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
2024-06-29 23:53:59 +00:00
|
|
|
"io/fs"
|
2024-06-03 16:49:13 +00:00
|
|
|
"log/slog"
|
|
|
|
"math"
|
2024-05-21 18:28:16 +00:00
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2024-06-03 16:49:13 +00:00
|
|
|
"slices"
|
2024-09-06 00:02:28 +00:00
|
|
|
"strings"
|
2024-05-21 18:28:16 +00:00
|
|
|
"testing"
|
|
|
|
|
2024-06-03 16:49:13 +00:00
|
|
|
"golang.org/x/exp/maps"
|
2024-08-01 21:52:15 +00:00
|
|
|
|
|
|
|
"github.com/ollama/ollama/llm"
|
2024-05-21 18:28:16 +00:00
|
|
|
)
|
|
|
|
|
2024-09-06 00:02:28 +00:00
|
|
|
type tensorData struct {
|
|
|
|
Offsets []int `json:"data_offsets"`
|
|
|
|
Type string `json:"dtype"`
|
|
|
|
Shape []int `json:"shape"`
|
|
|
|
}
|
|
|
|
|
2024-06-29 23:53:59 +00:00
|
|
|
func convertFull(t *testing.T, fsys fs.FS) (*os.File, llm.KV, llm.Tensors) {
|
2024-05-21 18:28:16 +00:00
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
f, err := os.CreateTemp(t.TempDir(), "f16")
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
|
2024-08-23 18:29:56 +00:00
|
|
|
if err := ConvertModel(fsys, f); err != nil {
|
2024-05-21 18:28:16 +00:00
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
r, err := os.Open(f.Name())
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
2024-06-03 16:49:13 +00:00
|
|
|
t.Cleanup(func() { r.Close() })
|
2024-05-21 18:28:16 +00:00
|
|
|
|
2024-06-03 16:49:13 +00:00
|
|
|
m, _, err := llm.DecodeGGML(r, math.MaxInt)
|
2024-05-21 18:28:16 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
2024-06-03 16:49:13 +00:00
|
|
|
if _, err := r.Seek(0, io.SeekStart); err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return r, m.KV(), m.Tensors()
|
|
|
|
}
|
|
|
|
|
2024-08-23 18:29:56 +00:00
|
|
|
func generateResultsJSON(t *testing.T, f *os.File, kv llm.KV, tensors llm.Tensors) map[string]string {
|
|
|
|
actual := make(map[string]string)
|
|
|
|
for k, v := range kv {
|
|
|
|
if s, ok := v.(json.Marshaler); !ok {
|
|
|
|
actual[k] = fmt.Sprintf("%v", v)
|
|
|
|
} else {
|
|
|
|
bts, err := json.Marshal(s)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
actual[k] = fmt.Sprintf("%x", sha256.Sum256(bts))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, tensor := range tensors.Items {
|
|
|
|
sha256sum := sha256.New()
|
|
|
|
sr := io.NewSectionReader(f, int64(tensors.Offset+tensor.Offset), int64(tensor.Size()))
|
|
|
|
if _, err := io.Copy(sha256sum, sr); err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
actual[tensor.Name] = hex.EncodeToString(sha256sum.Sum(nil))
|
|
|
|
}
|
|
|
|
|
|
|
|
return actual
|
|
|
|
}
|
|
|
|
|
2024-06-03 16:49:13 +00:00
|
|
|
func TestMain(m *testing.M) {
|
|
|
|
var level slog.Level
|
|
|
|
flag.TextVar(&level, "level", slog.LevelInfo, "log level")
|
|
|
|
flag.Parse()
|
|
|
|
slog.SetLogLoggerLevel(level)
|
|
|
|
os.Exit(m.Run())
|
2024-05-21 18:28:16 +00:00
|
|
|
}
|
|
|
|
|
2024-08-27 17:45:39 +00:00
|
|
|
func TestConvertModel(t *testing.T) {
|
2024-06-03 16:49:13 +00:00
|
|
|
cases := []string{
|
|
|
|
"Meta-Llama-3-8B-Instruct",
|
2024-07-29 21:53:02 +00:00
|
|
|
"Meta-Llama-3.1-8B-Instruct",
|
2024-06-03 16:49:13 +00:00
|
|
|
"Mistral-7B-Instruct-v0.2",
|
|
|
|
"Mixtral-8x7B-Instruct-v0.1",
|
|
|
|
"gemma-2b-it",
|
2024-09-06 00:02:28 +00:00
|
|
|
"gemma-2-2b-it",
|
2024-06-03 22:53:58 +00:00
|
|
|
// microsoft/Phi-3-mini-128-instruct@d548c233192db00165d842bf8edff054bb3212f8
|
|
|
|
"Phi-3-mini-128k-instruct",
|
2024-06-06 15:59:04 +00:00
|
|
|
"all-MiniLM-L6-v2",
|
2024-06-28 20:27:05 +00:00
|
|
|
"gemma-2-9b-it",
|
2024-05-21 18:28:16 +00:00
|
|
|
}
|
|
|
|
|
2024-06-03 16:49:13 +00:00
|
|
|
for i := range cases {
|
|
|
|
tt := cases[i]
|
|
|
|
t.Run(tt, func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
p := filepath.Join("testdata", tt)
|
|
|
|
if testing.Short() {
|
|
|
|
t.Skip("skipping in short mode")
|
|
|
|
} else if _, err := os.Stat(p); err != nil {
|
2024-05-21 18:28:16 +00:00
|
|
|
t.Skipf("%s not found", p)
|
|
|
|
}
|
|
|
|
|
2024-06-29 23:53:59 +00:00
|
|
|
f, kv, tensors := convertFull(t, os.DirFS(p))
|
2024-08-23 18:29:56 +00:00
|
|
|
actual := generateResultsJSON(t, f, kv, tensors)
|
2024-05-21 18:28:16 +00:00
|
|
|
|
2024-06-03 16:49:13 +00:00
|
|
|
expectFile, err := os.Open(filepath.Join("testdata", fmt.Sprintf("%s.json", tt)))
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
2024-05-21 18:28:16 +00:00
|
|
|
}
|
|
|
|
|
2024-06-03 16:49:13 +00:00
|
|
|
var expect map[string]string
|
|
|
|
if err := json.NewDecoder(expectFile).Decode(&expect); err != nil {
|
|
|
|
t.Fatal(err)
|
2024-05-21 18:28:16 +00:00
|
|
|
}
|
|
|
|
|
2024-06-03 16:49:13 +00:00
|
|
|
keys := maps.Keys(expect)
|
|
|
|
slices.Sort(keys)
|
|
|
|
for _, k := range keys {
|
|
|
|
if v, ok := actual[k]; !ok {
|
|
|
|
t.Errorf("missing %s", k)
|
|
|
|
} else if v != expect[k] {
|
|
|
|
t.Errorf("unexpected %s: want %s, got %s", k, expect[k], v)
|
|
|
|
}
|
2024-05-21 18:28:16 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2024-08-23 18:29:56 +00:00
|
|
|
|
2024-09-06 00:02:28 +00:00
|
|
|
func TestConvertInvalidTensorNames(t *testing.T) {
|
2024-08-28 00:54:04 +00:00
|
|
|
f, err := os.CreateTemp(t.TempDir(), "testmodel")
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
tempDir := t.TempDir()
|
2024-09-06 00:02:28 +00:00
|
|
|
|
|
|
|
td := map[string]*tensorData{}
|
|
|
|
offset := 4096
|
|
|
|
|
|
|
|
td["model.layers.0.self_attn.q_proj.weight"] = &tensorData{
|
|
|
|
Offsets: []int{0, offset},
|
|
|
|
Type: "F32",
|
|
|
|
Shape: []int{4096, 4096},
|
|
|
|
}
|
|
|
|
td["blk.0.attn_q.weight"] = &tensorData{
|
|
|
|
Offsets: []int{offset, offset * 2},
|
|
|
|
Type: "F32",
|
|
|
|
Shape: []int{4096, 4096},
|
|
|
|
}
|
|
|
|
generateSafetensorTestData(t, tempDir, td)
|
2024-08-28 00:54:04 +00:00
|
|
|
|
|
|
|
err = ConvertModel(os.DirFS(tempDir), f)
|
2024-09-06 00:02:28 +00:00
|
|
|
if err == nil || !strings.HasPrefix(err.Error(), "duplicate tensor name") {
|
2024-08-28 00:54:04 +00:00
|
|
|
t.Errorf("expected error but didn't get one")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-06 00:02:28 +00:00
|
|
|
func TestConvertInvalidDatatype(t *testing.T) {
|
|
|
|
f, err := os.CreateTemp(t.TempDir(), "testmodel")
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
2024-08-28 00:54:04 +00:00
|
|
|
}
|
2024-09-06 00:02:28 +00:00
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
tempDir := t.TempDir()
|
2024-08-28 00:54:04 +00:00
|
|
|
|
|
|
|
td := map[string]*tensorData{}
|
2024-09-06 00:02:28 +00:00
|
|
|
offset := 4096 * 14336
|
|
|
|
|
2024-08-28 00:54:04 +00:00
|
|
|
td["model.layers.0.mlp.down_proj.weight"] = &tensorData{
|
|
|
|
Offsets: []int{0, offset},
|
|
|
|
Type: "I8",
|
|
|
|
Shape: []int{4096, 14336},
|
|
|
|
}
|
|
|
|
td["model.layers.0.mlp.down_proj.weight_format"] = &tensorData{
|
|
|
|
Offsets: []int{offset, offset},
|
|
|
|
Type: "U8",
|
|
|
|
Shape: []int{},
|
|
|
|
}
|
2024-09-06 00:02:28 +00:00
|
|
|
generateSafetensorTestData(t, tempDir, td)
|
2024-08-28 00:54:04 +00:00
|
|
|
|
2024-09-06 00:02:28 +00:00
|
|
|
err = ConvertModel(os.DirFS(tempDir), f)
|
|
|
|
if err == nil || err.Error() != "unsupported safetensors model" {
|
|
|
|
t.Errorf("expected error but didn't get one")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func generateSafetensorTestData(t *testing.T, tempDir string, tensorData map[string]*tensorData) {
|
|
|
|
data, err := json.Marshal(tensorData)
|
2024-08-28 00:54:04 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var buf bytes.Buffer
|
|
|
|
|
|
|
|
l := int64(len(data))
|
|
|
|
err = binary.Write(&buf, binary.LittleEndian, l)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = buf.Write(data)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
fdata, err := os.Create(filepath.Join(tempDir, "model-00001-of-00001.safetensors"))
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
defer fdata.Close()
|
|
|
|
|
|
|
|
_, err = fdata.Write(buf.Bytes())
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
configData := `
|
|
|
|
{
|
|
|
|
"architectures": [
|
|
|
|
"LlamaForCausalLM"
|
|
|
|
]
|
|
|
|
}
|
|
|
|
`
|
|
|
|
|
|
|
|
f, err := os.Create(filepath.Join(tempDir, "config.json"))
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
_, err = f.WriteString(configData)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
tokenizerData := `
|
|
|
|
{
|
|
|
|
}
|
|
|
|
`
|
|
|
|
|
|
|
|
f, err = os.Create(filepath.Join(tempDir, "tokenizer.json"))
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
_, err = f.WriteString(tokenizerData)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-23 18:29:56 +00:00
|
|
|
func TestConvertAdapter(t *testing.T) {
|
|
|
|
type AdapterCase struct {
|
|
|
|
Name string
|
|
|
|
BaseKV map[string]any
|
|
|
|
Expected map[string]string
|
|
|
|
}
|
|
|
|
|
|
|
|
cases := []AdapterCase{
|
|
|
|
{
|
|
|
|
Name: "discollama",
|
|
|
|
BaseKV: map[string]any{
|
|
|
|
"general.architecture": "llama",
|
|
|
|
"llama.attention.head_count": uint32(32),
|
|
|
|
"llama.attention.head_count_kv": uint32(8),
|
|
|
|
},
|
|
|
|
Expected: map[string]string{
|
|
|
|
"general.architecture": "llama",
|
|
|
|
"general.file_type": "1",
|
|
|
|
"general.parameter_count": "106496",
|
|
|
|
"general.type": "adapter",
|
|
|
|
"general.version": "v0.2",
|
|
|
|
"adapter.lora.alpha": "16",
|
|
|
|
"adapter.type": "lora",
|
|
|
|
"llama.attention.head_count": "32",
|
|
|
|
"llama.attention.head_count_kv": "8",
|
|
|
|
"blk.31.attn_q.weight.lora_a": "0eb3318b02cd313429bcc7621b539fdbb10240fea190c56c9e5f93fcd37a4e50",
|
|
|
|
"blk.31.attn_q.weight.lora_b": "0eb3318b02cd313429bcc7621b539fdbb10240fea190c56c9e5f93fcd37a4e50",
|
|
|
|
"blk.31.attn_v.weight.lora_a": "0eb3318b02cd313429bcc7621b539fdbb10240fea190c56c9e5f93fcd37a4e50",
|
|
|
|
"blk.31.attn_v.weight.lora_b": "071dcafe89df065d6e1c935ecb8fdf6479b3c202eb912e7da938597673ff5857",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, c := range cases {
|
|
|
|
t.Run(c.Name, func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
f, err := os.CreateTemp(t.TempDir(), "f16")
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
tempDir := t.TempDir()
|
|
|
|
generateLoraTestData(t, tempDir)
|
|
|
|
|
|
|
|
if err = ConvertAdapter(os.DirFS(tempDir), f, c.BaseKV); err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
r, err := os.Open(f.Name())
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
defer r.Close()
|
|
|
|
|
|
|
|
m, _, err := llm.DecodeGGML(r, math.MaxInt)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, err := r.Seek(0, io.SeekStart); err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
actual := generateResultsJSON(t, r, m.KV(), m.Tensors())
|
|
|
|
|
|
|
|
keys := maps.Keys(c.Expected)
|
|
|
|
slices.Sort(keys)
|
|
|
|
for _, k := range keys {
|
|
|
|
if v, ok := actual[k]; !ok {
|
|
|
|
t.Errorf("missing %s", k)
|
|
|
|
} else if v != c.Expected[k] {
|
|
|
|
t.Errorf("unexpected %s: want %s, got %s", k, c.Expected[k], v)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func generateLoraTestData(t *testing.T, tempDir string) {
|
|
|
|
offset := 4096 * 8 * 4
|
|
|
|
|
|
|
|
td := map[string]*tensorData{"__metadata__": nil}
|
|
|
|
td["model.layers.31.self_attn.q_proj.lora_a"] = &tensorData{
|
|
|
|
Offsets: []int{0, offset},
|
|
|
|
Type: "F32",
|
|
|
|
Shape: []int{4096, 8},
|
|
|
|
}
|
|
|
|
td["model.layers.31.self_attn.q_proj.lora_b"] = &tensorData{
|
|
|
|
Offsets: []int{offset, offset * 2},
|
|
|
|
Type: "F32",
|
|
|
|
Shape: []int{8, 4096},
|
|
|
|
}
|
|
|
|
td["model.layers.31.self_attn.v_proj.lora_a"] = &tensorData{
|
|
|
|
Offsets: []int{offset * 2, offset * 3},
|
|
|
|
Type: "F32",
|
|
|
|
Shape: []int{4096, 8},
|
|
|
|
}
|
|
|
|
td["model.layers.31.self_attn.v_proj.lora_b"] = &tensorData{
|
|
|
|
Offsets: []int{offset * 3, offset*3 + 8*1024*4},
|
|
|
|
Type: "F32",
|
|
|
|
Shape: []int{8, 1024},
|
|
|
|
}
|
|
|
|
|
|
|
|
data, err := json.Marshal(td)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var buf bytes.Buffer
|
|
|
|
|
|
|
|
l := int64(len(data))
|
|
|
|
err = binary.Write(&buf, binary.LittleEndian, l)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = buf.Write(data)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// write some data for the tensors
|
|
|
|
|
|
|
|
ones := make([]float32, 4096*8)
|
|
|
|
for i := range ones {
|
|
|
|
ones[i] = float32(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
for range 3 {
|
|
|
|
err = binary.Write(&buf, binary.LittleEndian, ones)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
ones = make([]float32, 1024*8)
|
|
|
|
for i := range ones {
|
|
|
|
ones[i] = float32(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = binary.Write(&buf, binary.LittleEndian, ones)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
fdata, err := os.Create(filepath.Join(tempDir, "adapters.safetensors"))
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
defer fdata.Close()
|
|
|
|
|
|
|
|
_, err = fdata.Write(buf.Bytes())
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
configData := `
|
|
|
|
{
|
|
|
|
"adapter_path": "adapters-test",
|
|
|
|
"batch_size": 8,
|
|
|
|
"config": "config-tiny.json",
|
|
|
|
"data": "../discollama-completion",
|
|
|
|
"grad_checkpoint": null,
|
|
|
|
"iters": 1000,
|
|
|
|
"learning_rate": 1e-05,
|
|
|
|
"lora_layers": 1,
|
|
|
|
"lora_parameters": {
|
|
|
|
"rank": 8,
|
|
|
|
"alpha": 16,
|
|
|
|
"dropout": 0.0,
|
|
|
|
"scale": 2.0
|
|
|
|
},
|
|
|
|
"lr_schedule": null,
|
|
|
|
"max_seq_length": 2048,
|
|
|
|
"model": "/Users/pdevine/git/Meta-Llama-3-8B-Instruct",
|
|
|
|
"resume_adapter_file": null,
|
|
|
|
"save_every": 100,
|
|
|
|
"seed": 0,
|
|
|
|
"steps_per_eval": 200,
|
|
|
|
"steps_per_report": 10,
|
|
|
|
"test": false,
|
|
|
|
"test_batches": 500,
|
|
|
|
"train": true,
|
|
|
|
"use_dora": false,
|
|
|
|
"val_batches": 25
|
|
|
|
}
|
|
|
|
`
|
|
|
|
f, err := os.Create(filepath.Join(tempDir, "adapter_config.json"))
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
_, err = f.WriteString(configData)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
}
|