Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

122 lines
3.0 KiB
Go

//go:build embeddings_gomlx
package embedding
import (
"context"
"fmt"
"os"
"path/filepath"
"sync"
"github.com/knights-analytics/hugot"
"github.com/knights-analytics/hugot/pipelines"
"github.com/zzet/gortex/internal/platform"
)
const gomlxModelName = "sentence-transformers/all-MiniLM-L6-v2"
// GoMLXProvider uses Hugot with the XLA/GoMLX backend for transformer embeddings.
// XLA/PJRT plugin auto-downloads on first use (~100MB).
type GoMLXProvider struct {
session *hugot.Session
pipeline *pipelines.FeatureExtractionPipeline
dims int
truncator *tokenTruncator
mu sync.Mutex
}
func newGoMLXProvider() (Provider, error) {
session, err := hugot.NewXLASession(context.Background())
if err != nil {
return nil, fmt.Errorf("gomlx/xla session: %w", err)
}
modelPath, err := ensureGoMLXModel()
if err != nil {
_ = session.Destroy()
return nil, fmt.Errorf("gomlx model: %w", err)
}
config := hugot.FeatureExtractionConfig{
ModelPath: modelPath,
Name: "gortex-embeddings-gomlx",
Options: []hugot.FeatureExtractionOption{
pipelines.WithNormalization(),
},
}
pipeline, err := hugot.NewPipeline(session, config)
if err != nil {
_ = session.Destroy()
return nil, fmt.Errorf("gomlx pipeline: %w", err)
}
// Belt-and-suspenders token truncation: the XLA path's Rust tokenizer
// already truncates, but keeping the client-side cap here matches the
// pure-Go provider and covers a degraded tokenizer. See hugot.go.
truncator, terr := newTokenTruncator(modelPath)
if terr != nil {
fmt.Fprintf(os.Stderr, "[gortex embedding] %v\n", terr)
}
return &GoMLXProvider{
session: session,
pipeline: pipeline,
dims: 384,
truncator: truncator,
}, nil
}
func (p *GoMLXProvider) Embed(ctx context.Context, text string) ([]float32, error) {
vecs, err := p.EmbedBatch(ctx, []string{text})
if err != nil {
return nil, err
}
if len(vecs) == 0 {
return nil, fmt.Errorf("gomlx returned no embeddings")
}
return vecs[0], nil
}
func (p *GoMLXProvider) EmbedBatch(ctx context.Context, texts []string) ([][]float32, error) {
p.mu.Lock()
defer p.mu.Unlock()
texts = p.truncator.TruncateAll(texts)
output, err := p.pipeline.RunPipeline(ctx, texts)
if err != nil {
return nil, fmt.Errorf("gomlx run: %w", err)
}
if err := validateBatch("gomlx", texts, output.Embeddings, p.dims); err != nil {
return nil, err
}
return output.Embeddings, nil
}
func (p *GoMLXProvider) Dimensions() int { return p.dims }
func (p *GoMLXProvider) Close() error {
if p.session != nil {
return p.session.Destroy()
}
return nil
}
func ensureGoMLXModel() (string, error) {
dest := platform.ModelsDir()
modelDir := filepath.Join(dest, "sentence-transformers_all-MiniLM-L6-v2")
if _, err := os.Stat(filepath.Join(modelDir, "tokenizer.json")); err == nil {
return modelDir, nil
}
path, err := hugot.DownloadModel(context.Background(), gomlxModelName, dest, hugot.NewDownloadOptions())
if err != nil {
return "", fmt.Errorf("download model: %w", err)
}
return path, nil
}