c2e8cbaa14
If we get a request with a zero length image, it will result in an out-of-bounds error when we pass the data to the image encoder.
183 lines
3.9 KiB
Go
183 lines
3.9 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"hash/maphash"
|
|
"log/slog"
|
|
"slices"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/ollama/ollama/llama"
|
|
)
|
|
|
|
const imageCacheSize = 4
|
|
|
|
type ImageContext struct {
|
|
// mu is required to be held when generating embeddings or accessing the cache
|
|
mu sync.Mutex
|
|
|
|
clip *llama.ClipContext
|
|
mllama *llama.MllamaContext
|
|
|
|
// cache of images to embeddings
|
|
images []imageCache
|
|
imageHash maphash.Hash
|
|
}
|
|
|
|
func NewImageContext(llamaContext *llama.Context, modelPath string) (*ImageContext, error) {
|
|
arch, err := llama.GetModelArch(modelPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to determine vision architecture: %w (%s)", err, modelPath)
|
|
}
|
|
|
|
var c ImageContext
|
|
if arch == "clip" {
|
|
c.clip, err = llama.NewClipContext(llamaContext, modelPath)
|
|
} else if arch == "mllama" {
|
|
c.mllama, err = llama.NewMllamaContext(llamaContext, modelPath)
|
|
} else {
|
|
return nil, fmt.Errorf("unknown vision model architecture: %s", arch)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
c.images = make([]imageCache, imageCacheSize)
|
|
|
|
return &c, nil
|
|
}
|
|
|
|
func (c *ImageContext) Free(modelPath string) {
|
|
if c == nil {
|
|
return
|
|
}
|
|
|
|
if c.clip != nil {
|
|
c.clip.Free()
|
|
}
|
|
if c.mllama != nil {
|
|
c.mllama.Free()
|
|
}
|
|
}
|
|
|
|
func (c *ImageContext) NewEmbed(llamaContext *llama.Context, data []byte, aspectRatioId int) ([][]float32, error) {
|
|
if c == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
if len(data) <= 0 {
|
|
return nil, errors.New("received zero length image")
|
|
}
|
|
|
|
hash := c.hashImage(data)
|
|
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
embed, err := c.findImage(hash)
|
|
if err != nil {
|
|
if c.mllama != nil {
|
|
embed, err = c.mllama.NewEmbed(llamaContext, data, aspectRatioId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else if c.clip != nil {
|
|
embed, err = c.clip.NewEmbed(llamaContext, data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
return nil, errors.New("received image but vision model not loaded")
|
|
}
|
|
|
|
c.addImage(hash, embed)
|
|
}
|
|
|
|
return embed, nil
|
|
}
|
|
|
|
func (c *ImageContext) BatchSize(configuredBatchSize int) int {
|
|
// If images are not supported, we don't need to allocate embedding batches
|
|
if c == nil {
|
|
return 0
|
|
}
|
|
|
|
// Mllama maps an image to 1 embedding token (llava creates many tokens)
|
|
// and doesn't support more than a single image per request.
|
|
// The embeddings are large (100 MB), so allocating a big batch can fail
|
|
// on some systems
|
|
if c.mllama != nil {
|
|
return 1
|
|
}
|
|
|
|
return configuredBatchSize
|
|
}
|
|
|
|
func (c *ImageContext) EmbedSize(llamaContext *llama.Context) int {
|
|
if c != nil && c.mllama != nil {
|
|
return c.mllama.EmbedSize(llamaContext)
|
|
} else {
|
|
return llamaContext.Model().NEmbd()
|
|
}
|
|
}
|
|
|
|
func (c *ImageContext) NeedCrossAttention(inputs ...input) bool {
|
|
if c == nil || c.mllama == nil {
|
|
return false
|
|
}
|
|
|
|
return slices.ContainsFunc(inputs, func(input input) bool {
|
|
return input.embed != nil
|
|
})
|
|
}
|
|
|
|
type imageCache struct {
|
|
key uint64
|
|
val [][]float32
|
|
lastUsed time.Time
|
|
}
|
|
|
|
func (c *ImageContext) hashImage(image []byte) uint64 {
|
|
c.imageHash.Reset()
|
|
_, _ = c.imageHash.Write(image)
|
|
return c.imageHash.Sum64()
|
|
}
|
|
|
|
var errImageNotFound = errors.New("image not found in cache")
|
|
|
|
func (c *ImageContext) findImage(hash uint64) ([][]float32, error) {
|
|
for i := range c.images {
|
|
if c.images[i].key == hash {
|
|
slog.Debug("loading image embeddings from cache", "entry", i)
|
|
c.images[i].lastUsed = time.Now()
|
|
return c.images[i].val, nil
|
|
}
|
|
}
|
|
|
|
return nil, errImageNotFound
|
|
}
|
|
|
|
func (c *ImageContext) addImage(hash uint64, embed [][]float32) {
|
|
best := time.Now()
|
|
var bestImage int
|
|
|
|
for i := range c.images {
|
|
if c.images[i].key == hash {
|
|
bestImage = i
|
|
break
|
|
}
|
|
|
|
if c.images[i].lastUsed.Compare(best) < 0 {
|
|
best = c.images[i].lastUsed
|
|
bestImage = i
|
|
}
|
|
}
|
|
|
|
slog.Debug("storing image embeddings in cache", "entry", bestImage, "used", c.images[bestImage].lastUsed)
|
|
c.images[bestImage].key = hash
|
|
c.images[bestImage].val = embed
|
|
c.images[bestImage].lastUsed = time.Now()
|
|
}
|