From 52b7fd336972ab1f37fbd4b3d3c8e30d0466dc82 Mon Sep 17 00:00:00 2001 From: timkaye11 Date: Fri, 2 Jan 2026 21:37:35 -0800 Subject: [PATCH 1/7] wip t5gemma2 --- README.md | 21 + docs/T5GEMMA2.md | 291 +++++ e2e/harness_test.go | 23 +- e2e/t5gemma2_test.go | 380 +++++++ go.work | 3 +- openapi.yaml | 113 ++ pkg/termite/api.gen.go | 57 + pkg/termite/api.go | 9 + pkg/termite/embedder_registry.go | 25 + pkg/termite/lib/embeddings/t5gemma2.go | 494 +++++++++ pkg/termite/lib/embeddings/t5gemma2_stub.go | 97 ++ pkg/termite/lib/seq2seq/t5gemma2.go | 573 ++++++++++ pkg/termite/lib/seq2seq/t5gemma2_stub.go | 143 +++ pkg/termite/model_registry.go | 50 +- pkg/termite/seq2seq_registry.go | 92 +- scripts/export_model_to_registry.py | 211 +++- scripts/export_t5gemma2.py | 1073 +++++++++++++++++++ 17 files changed, 3622 insertions(+), 33 deletions(-) create mode 100644 docs/T5GEMMA2.md create mode 100644 e2e/t5gemma2_test.go create mode 100644 pkg/termite/lib/embeddings/t5gemma2.go create mode 100644 pkg/termite/lib/embeddings/t5gemma2_stub.go create mode 100644 pkg/termite/lib/seq2seq/t5gemma2.go create mode 100644 pkg/termite/lib/seq2seq/t5gemma2_stub.go create mode 100755 scripts/export_t5gemma2.py diff --git a/README.md b/README.md index ffbc6a8..7798fef 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,27 @@ embedder: Termite auto-selects the best available variant if not specified. +### T5Gemma-2 (Multimodal) + +T5Gemma-2 is Google's multimodal encoder-decoder model supporting text embeddings, image embeddings, and text generation. + +```bash +# Export model to ONNX (requires uv and HuggingFace login) +./scripts/export_t5gemma2.py \ + --model google/t5gemma-2-270m-270m \ + --output ~/.termite/models/rewriters/google/t5gemma-2-270m + +# Use for embeddings +curl -X POST http://localhost:8080/v1/embeddings \ + -d '{"model": "google/t5gemma-2-270m", "input": ["Hello world"]}' + +# Use for text generation +curl -X POST http://localhost:8080/api/rewrite \ + -d '{"model": "google/t5gemma-2-270m", "texts": ["Summarize: ..."]}' +``` + +See [docs/T5GEMMA2.md](docs/T5GEMMA2.md) for full setup instructions. + ## Kubernetes Operator Deploy on GKE with TPU support using the Termite Operator. diff --git a/docs/T5GEMMA2.md b/docs/T5GEMMA2.md new file mode 100644 index 0000000..cc2203f --- /dev/null +++ b/docs/T5GEMMA2.md @@ -0,0 +1,291 @@ +# T5Gemma-2 Model Support + +T5Gemma-2 is Google's multimodal encoder-decoder model from the Gemma 3 family, trained with the UL2 objective. This document explains how to export and run T5Gemma-2 models in Termite. + +## Features + +- **Text Embeddings**: 640-dimensional vectors from the encoder +- **Image Embeddings**: 640-dimensional vectors from SigLIP vision encoder +- **Text Generation**: Seq2seq generation for summarization, Q&A, rewriting +- **Multimodal**: Combined text + image understanding + +## Prerequisites + +### 1. Install uv (Python Package Manager) + +The export script uses [uv](https://github.com/astral-sh/uv) for hermetic, reproducible Python environments: + +```bash +# macOS +brew install uv + +# Linux/WSL +curl -LsSf https://astral.sh/uv/install.sh | sh +``` + +### 2. About optimum-onnx (Optional) + +**The export script is self-contained** - it applies ONNX compatibility patches manually and does NOT require optimum-onnx. + +If you prefer using `optimum-cli export onnx` instead of our script, we have a fork with T5Gemma-2 support: + +```bash +# Only needed if using optimum-cli (not our export script) +git clone https://github.com/timkaye11/optimum-onnx.git +cd optimum-onnx +git checkout add-t5gemma2-support +pip install -e . + +# Then export with optimum-cli +optimum-cli export onnx --model google/t5gemma-2-270m-270m ./output +``` + +The fork adds: +- `T5Gemma2OnnxConfig` in `optimum/exporters/onnx/model_configs.py` +- Removes transformers version upper bound (T5Gemma-2 requires transformers >= 4.58.0) + +### 3. HuggingFace Access + +T5Gemma-2 models require accepting Google's license on HuggingFace: + +1. Go to https://huggingface.co/google/t5gemma-2-270m-270m +2. Accept the license agreement +3. Get a HuggingFace token from https://huggingface.co/settings/tokens + +```bash +# Login to HuggingFace +huggingface-cli login +``` + +## Exporting Models + +### Quick Start + +```bash +cd termite + +# Export the 270M model (recommended for development, ~6.7GB) +./scripts/export_t5gemma2.py \ + --model google/t5gemma-2-270m-270m \ + --output ~/.termite/models/rewriters/google/t5gemma-2-270m + +# Verify the export with a test +./scripts/export_t5gemma2.py \ + --model google/t5gemma-2-270m-270m \ + --output ~/.termite/models/rewriters/google/t5gemma-2-270m \ + --test +``` + +### Available Models + +| Model | Total Params | Size | Use Case | +|-------|--------------|------|----------| +| `google/t5gemma-2-270m-270m` | ~800M | ~6.7GB | Development, testing | +| `google/t5gemma-2-1b-1b` | ~2B | ~15GB | Balanced quality/speed | +| `google/t5gemma-2-4b-4b` | ~8B | ~50GB | Best quality | + +### Export Options + +```bash +./scripts/export_t5gemma2.py --help + +Options: + --model MODEL HuggingFace model ID (required) + --output DIR Output directory for ONNX files (required) + --test Run validation tests after export + --test-input TEXT Custom test input for generation + --skip-encoder Skip encoder export (if already exported) + --skip-decoder Skip decoder export (if already exported) + --skip-vision Skip vision encoder export +``` + +### Exported Files + +After export, the output directory contains: + +``` +~/.termite/models/rewriters/google/t5gemma-2-270m/ +├── encoder.onnx # Text encoder (1.07 GB) +├── decoder.onnx # Decoder with KV cache (1.74 GB) +├── decoder-init.onnx # Decoder for first token (1.74 GB) +├── vision_encoder.onnx # SigLIP vision encoder (1.67 GB) +├── embedding_layer.onnx # Word embeddings fallback (671 MB) +├── config.json # HuggingFace model config +├── t5gemma2_config.json # Termite-specific config +├── tokenizer.json # SentencePiece tokenizer (33 MB) +├── tokenizer_config.json # Tokenizer settings +└── preprocessor_config.json # Vision preprocessing +``` + +## Running in Termite + +### Configuration + +Set the rewriters directory in your termite config: + +```yaml +# config.yaml +rewriters_dir: ~/.termite/models/rewriters +keep_alive: 5m +max_loaded_models: 2 +``` + +Or via environment variable: + +```bash +export TERMITE_REWRITERS_DIR=~/.termite/models/rewriters +``` + +### Starting the Server + +```bash +# Build with ONNX support +CGO_ENABLED=1 go build -tags="onnx,ORT" -o termite ./pkg/termite/cmd + +# Run +./termite run --config config.yaml +``` + +### Verifying Model Discovery + +```bash +# List available models +curl http://localhost:8080/api/models | jq + +# Should show: +# { +# "rewriters": ["google/t5gemma-2-270m"], +# ... +# } +``` + +## API Usage + +### Text Embeddings + +```bash +curl -X POST http://localhost:8080/v1/embeddings \ + -H "Content-Type: application/json" \ + -d '{ + "model": "google/t5gemma-2-270m", + "input": ["What is machine learning?", "How do neural networks work?"] + }' +``` + +Response: +```json +{ + "data": [ + {"embedding": [0.123, -0.456, ...], "index": 0}, + {"embedding": [0.789, -0.012, ...], "index": 1} + ], + "model": "google/t5gemma-2-270m", + "usage": {"prompt_tokens": 12, "total_tokens": 12} +} +``` + +### Image Embeddings + +```bash +# Base64-encoded image +curl -X POST http://localhost:8080/v1/embeddings \ + -H "Content-Type: application/json" \ + -d '{ + "model": "google/t5gemma-2-270m", + "input": [{"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}}] + }' +``` + +### Text Generation (Rewriting) + +```bash +curl -X POST http://localhost:8080/api/rewrite \ + -H "Content-Type: application/json" \ + -d '{ + "model": "google/t5gemma-2-270m", + "texts": ["Summarize: The quick brown fox jumps over the lazy dog."] + }' +``` + +Response: +```json +{ + "outputs": [ + {"text": "A fox jumps over a dog.", "tokens": [...]} + ], + "model": "google/t5gemma-2-270m" +} +``` + +## Troubleshooting + +### Export Fails with "transformers version" Error + +Ensure you're using the forked optimum-onnx: +```bash +cd /path/to/optimum-onnx +git checkout add-t5gemma2-support +pip install -e . +``` + +### "Model not found" in Termite + +Check the directory structure: +```bash +ls -la ~/.termite/models/rewriters/google/t5gemma-2-270m/ + +# Must contain: +# - encoder.onnx +# - decoder.onnx +# - decoder-init.onnx +# - config.json with "model_type": "t5gemma2" +``` + +### Out of Memory During Export + +T5Gemma-2 requires significant RAM for export: +- 270M model: ~8GB RAM +- 1B model: ~16GB RAM +- 4B model: ~32GB RAM + +Use `--skip-vision` if you only need text capabilities. + +### Slow Inference + +Ensure you're using the ONNX build: +```bash +# Check build tags +./termite version + +# Should show: Backend: onnx +``` + +## Technical Details + +### Model Architecture + +- **Encoder**: 18 layers, 4 attention heads (GQA: 1 KV head), 640 hidden size +- **Decoder**: 18 layers, merged self/cross attention, alternating sliding window +- **Vision**: SigLIP with 27 layers, 896x896 input, 256 tokens per image +- **Context**: 128K encoder tokens, 32K decoder tokens + +### ONNX Export Patches + +The export script applies several patches for ONNX compatibility: +1. Disables torch.dynamo (incompatible with tracing) +2. Replaces vmap-based attention masks with broadcasting +3. Sets sliding window to large value during export +4. Forces eager attention mode + +### Known Limitations + +1. **GenerateMultimodal**: Currently validates images but doesn't inject vision embeddings into generation (text generation only uses `` placeholder tokens) +2. **KV Cache**: decoder.onnx currently copies decoder-init.onnx (full KV cache optimization is complex) +3. **Fused Embeddings**: Text and image embeddings are separate (no fused text+image vectors yet) + +## References + +- [HuggingFace Model Card](https://huggingface.co/google/t5gemma-2-270m-270m) +- [Transformers Documentation](https://huggingface.co/docs/transformers/model_doc/t5gemma2) +- [Google Blog Post](https://blog.google/technology/developers/t5gemma-2/) +- [optimum-onnx Fork](https://github.com/timkaye11/optimum-onnx/tree/add-t5gemma2-support) diff --git a/e2e/harness_test.go b/e2e/harness_test.go index 95bb0d5..f5092bc 100644 --- a/e2e/harness_test.go +++ b/e2e/harness_test.go @@ -78,16 +78,21 @@ func TestMain(m *testing.M) { fmt.Printf("E2E Test Setup: Using models directory: %s\n", testModelsDir) - // Download models from registry - if err := downloadTestModels(); err != nil { - fmt.Fprintf(os.Stderr, "Failed to download test models: %v\n", err) - os.Exit(1) - } + // Skip model downloads if SKIP_MODEL_DOWNLOAD is set (useful for running individual tests) + if os.Getenv("SKIP_MODEL_DOWNLOAD") != "true" { + // Download models from registry + if err := downloadTestModels(); err != nil { + fmt.Fprintf(os.Stderr, "Failed to download test models: %v\n", err) + os.Exit(1) + } - // Download HuggingFace models - if err := downloadHuggingFaceModels(); err != nil { - fmt.Fprintf(os.Stderr, "Failed to download HuggingFace models: %v\n", err) - os.Exit(1) + // Download HuggingFace models + if err := downloadHuggingFaceModels(); err != nil { + fmt.Fprintf(os.Stderr, "Failed to download HuggingFace models: %v\n", err) + os.Exit(1) + } + } else { + fmt.Printf("Skipping model downloads (SKIP_MODEL_DOWNLOAD=true)\n") } // Run tests diff --git a/e2e/t5gemma2_test.go b/e2e/t5gemma2_test.go new file mode 100644 index 0000000..4b9149a --- /dev/null +++ b/e2e/t5gemma2_test.go @@ -0,0 +1,380 @@ +//go:build onnx && ORT + +// Copyright 2025 Antfly, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package e2e + +import ( + "context" + "os" + "path/filepath" + "strconv" + "testing" + "time" + + "github.com/antflydb/termite/pkg/client" + "github.com/antflydb/termite/pkg/termite" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" +) + +// T5Gemma-2 model name using owner/model format +const t5Gemma2ModelName = "google/t5gemma-2-270m" + +// T5Gemma-2 hidden size (640). The actual embedding dimension may vary +// based on sequence length as the encoder returns token-level embeddings. +const t5Gemma2HiddenSize = 640 + +// getT5Gemma2ModelsDir returns the models directory, preferring ~/.termite/models +// if TERMITE_MODELS_DIR is not set and the model exists there. +func getT5Gemma2ModelsDir(t *testing.T) string { + // First check TERMITE_MODELS_DIR + if dir := os.Getenv("TERMITE_MODELS_DIR"); dir != "" { + return dir + } + + // Check test models dir from harness + testDir := getTestModelsDir() + if testDir != "" { + return testDir + } + + // Fall back to ~/.termite/models + homeDir, err := os.UserHomeDir() + if err != nil { + t.Logf("Could not get home directory: %v", err) + return "" + } + return filepath.Join(homeDir, ".termite", "models") +} + +// TestT5Gemma2EmbedderE2E tests the T5Gemma-2 embedder end-to-end. +// This test requires the model to be downloaded to the models directory. +func TestT5Gemma2EmbedderE2E(t *testing.T) { + if testing.Short() { + t.Skip("Skipping E2E test in short mode") + } + + // Check if model exists + modelsDir := getT5Gemma2ModelsDir(t) + if modelsDir == "" { + t.Skip("No models directory available") + } + + embeddersDir := filepath.Join(modelsDir, "embedders") + modelPath := filepath.Join(embeddersDir, "google", "t5gemma-2-270m") + if !fileExists(filepath.Join(modelPath, "encoder.onnx")) { + t.Skipf("T5Gemma-2 model not found at %s. Skipping E2E test.", modelPath) + } + + t.Logf("Using models directory: %s", modelsDir) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + + logger := zaptest.NewLogger(t) + port := findAvailablePort(t) + serverURL := "http://localhost:" + itoa(port) + + config := termite.Config{ + ApiUrl: serverURL, + ModelsDir: modelsDir, + } + + serverCtx, serverCancel := context.WithCancel(ctx) + defer serverCancel() + + readyC := make(chan struct{}) + serverDone := make(chan struct{}) + + go func() { + defer close(serverDone) + termite.RunAsTermite(serverCtx, logger, config, readyC) + }() + + select { + case <-readyC: + t.Log("Server is ready") + case <-time.After(120 * time.Second): + t.Fatal("Timeout waiting for server to be ready") + } + + termiteClient, err := client.NewTermiteClient(serverURL, nil) + require.NoError(t, err, "Creating client failed") + + // Run sub-tests + t.Run("ListModels", func(t *testing.T) { + testT5Gemma2ListModelsEmbedder(t, ctx, termiteClient) + }) + + t.Run("EmbedText", func(t *testing.T) { + testT5Gemma2EmbedText(t, ctx, termiteClient) + }) + + t.Run("EmbedMultipleTexts", func(t *testing.T) { + testT5Gemma2EmbedMultipleTexts(t, ctx, termiteClient) + }) + + t.Run("EmbeddingDimensions", func(t *testing.T) { + testT5Gemma2EmbeddingDimensions(t, ctx, termiteClient) + }) + + // Graceful shutdown + serverCancel() + select { + case <-serverDone: + t.Log("Server shutdown complete") + case <-time.After(30 * time.Second): + t.Log("Server shutdown timeout (may still be cleaning up)") + } +} + +func testT5Gemma2ListModelsEmbedder(t *testing.T, ctx context.Context, c *client.TermiteClient) { + t.Helper() + + models, err := c.ListModels(ctx) + require.NoError(t, err, "ListModels failed") + + // T5Gemma-2 should appear in embedders list + found := false + for _, name := range models.Embedders { + if name == t5Gemma2ModelName { + found = true + break + } + } + + require.True(t, found, "T5Gemma-2 should be in embedders list. Available: %v", models.Embedders) + t.Logf("Found T5Gemma-2 in embedders: %s", t5Gemma2ModelName) +} + +func testT5Gemma2EmbedText(t *testing.T, ctx context.Context, c *client.TermiteClient) { + t.Helper() + + inputs := []string{ + "The quick brown fox jumps over the lazy dog.", + } + + embeddings, err := c.Embed(ctx, t5Gemma2ModelName, inputs) + require.NoError(t, err, "T5Gemma-2 embedding failed") + require.Len(t, embeddings, 1, "Should return one embedding") + require.NotEmpty(t, embeddings[0], "Embedding should not be empty") + + t.Logf("Single text embedding: %d dimensions", len(embeddings[0])) +} + +func testT5Gemma2EmbedMultipleTexts(t *testing.T, ctx context.Context, c *client.TermiteClient) { + t.Helper() + + inputs := []string{ + "Machine learning is transforming artificial intelligence.", + "Neural networks can learn complex patterns from data.", + "Natural language processing enables computers to understand human language.", + } + + embeddings, err := c.Embed(ctx, t5Gemma2ModelName, inputs) + require.NoError(t, err, "T5Gemma-2 batch embedding failed") + require.Len(t, embeddings, len(inputs), "Should return embedding for each input") + + for i, emb := range embeddings { + require.NotEmpty(t, emb, "Embedding %d should not be empty", i) + t.Logf("Embedding %d: %d dimensions", i, len(emb)) + } +} + +func testT5Gemma2EmbeddingDimensions(t *testing.T, ctx context.Context, c *client.TermiteClient) { + t.Helper() + + inputs := []string{"Test embedding dimensions."} + + embeddings, err := c.Embed(ctx, t5Gemma2ModelName, inputs) + require.NoError(t, err, "Embedding failed") + require.Len(t, embeddings, 1, "Should return one embedding") + + // T5Gemma-2 returns token-level embeddings (seq_len * hidden_size). + // The dimension should be a multiple of the hidden size (640). + embDim := len(embeddings[0]) + assert.Greater(t, embDim, 0, "Embedding dimension should be positive") + assert.Equal(t, 0, embDim%t5Gemma2HiddenSize, + "T5Gemma-2 embedding dimension (%d) should be a multiple of hidden size (%d)", + embDim, t5Gemma2HiddenSize) + + seqLen := embDim / t5Gemma2HiddenSize + t.Logf("Verified embedding dimension: %d (seq_len=%d × hidden=%d)", embDim, seqLen, t5Gemma2HiddenSize) +} + +// TestT5Gemma2GeneratorE2E tests the T5Gemma-2 seq2seq generator end-to-end. +func TestT5Gemma2GeneratorE2E(t *testing.T) { + if testing.Short() { + t.Skip("Skipping E2E test in short mode") + } + + // Check if model exists in rewriters directory (seq2seq models) + modelsDir := getT5Gemma2ModelsDir(t) + if modelsDir == "" { + t.Skip("No models directory available") + } + + rewritersDir := filepath.Join(modelsDir, "rewriters") + modelPath := filepath.Join(rewritersDir, "google", "t5gemma-2-270m") + if !fileExists(filepath.Join(modelPath, "encoder.onnx")) { + t.Skipf("T5Gemma-2 generator model not found at %s. Skipping E2E test.", modelPath) + } + + t.Logf("Using models directory: %s", modelsDir) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + + logger := zaptest.NewLogger(t) + port := findAvailablePort(t) + serverURL := "http://localhost:" + itoa(port) + + config := termite.Config{ + ApiUrl: serverURL, + ModelsDir: modelsDir, + } + + serverCtx, serverCancel := context.WithCancel(ctx) + defer serverCancel() + + readyC := make(chan struct{}) + serverDone := make(chan struct{}) + + go func() { + defer close(serverDone) + termite.RunAsTermite(serverCtx, logger, config, readyC) + }() + + select { + case <-readyC: + t.Log("Server is ready") + case <-time.After(120 * time.Second): + t.Fatal("Timeout waiting for server to be ready") + } + + termiteClient, err := client.NewTermiteClient(serverURL, nil) + require.NoError(t, err, "Creating client failed") + + t.Run("ListModels", func(t *testing.T) { + testT5Gemma2ListModelsRewriter(t, ctx, termiteClient) + }) + + t.Run("RewriteText", func(t *testing.T) { + testT5Gemma2RewriteText(t, ctx, termiteClient) + }) + + t.Run("RewriteWithSummarizePrompt", func(t *testing.T) { + testT5Gemma2RewriteSummarize(t, ctx, termiteClient) + }) + + t.Run("RewriteMultipleInputs", func(t *testing.T) { + testT5Gemma2RewriteMultiple(t, ctx, termiteClient) + }) + + serverCancel() + select { + case <-serverDone: + t.Log("Server shutdown complete") + case <-time.After(30 * time.Second): + t.Log("Server shutdown timeout") + } +} + +func testT5Gemma2ListModelsRewriter(t *testing.T, ctx context.Context, c *client.TermiteClient) { + t.Helper() + + models, err := c.ListModels(ctx) + require.NoError(t, err, "ListModels failed") + + // T5Gemma-2 should appear in rewriters list + found := false + for _, name := range models.Rewriters { + if name == t5Gemma2ModelName { + found = true + break + } + } + + require.True(t, found, "T5Gemma-2 should be in rewriters list. Available: %v", models.Rewriters) + t.Logf("Found T5Gemma-2 in rewriters: %s", t5Gemma2ModelName) +} + +func testT5Gemma2RewriteText(t *testing.T, ctx context.Context, c *client.TermiteClient) { + t.Helper() + + inputs := []string{ + "The T5Gemma-2 model is a multimodal encoder-decoder architecture.", + } + + resp, err := c.RewriteText(ctx, t5Gemma2ModelName, inputs) + require.NoError(t, err, "T5Gemma-2 rewrite failed") + require.NotNil(t, resp, "Response should not be nil") + require.Equal(t, t5Gemma2ModelName, resp.Model, "Response model should match") + require.NotEmpty(t, resp.Texts, "Should return generated text") + + for i, texts := range resp.Texts { + require.NotEmpty(t, texts, "Output %d should have generated sequences", i) + t.Logf("Input %d generated: %q", i, texts[0]) + } +} + +func testT5Gemma2RewriteSummarize(t *testing.T, ctx context.Context, c *client.TermiteClient) { + t.Helper() + + // T5-style models typically work with task prefixes + inputs := []string{ + "Summarize: The quick brown fox jumps over the lazy dog. This is a classic pangram that contains every letter of the English alphabet at least once. It has been used for testing typewriters and fonts since the late 19th century.", + } + + resp, err := c.RewriteText(ctx, t5Gemma2ModelName, inputs) + require.NoError(t, err, "T5Gemma-2 summarize failed") + require.NotNil(t, resp, "Response should not be nil") + require.NotEmpty(t, resp.Texts, "Should return summarized text") + require.NotEmpty(t, resp.Texts[0], "First output should have generated text") + + t.Logf("Summarization result: %q", resp.Texts[0][0]) +} + +func testT5Gemma2RewriteMultiple(t *testing.T, ctx context.Context, c *client.TermiteClient) { + t.Helper() + + inputs := []string{ + "Hello world", + "Translate this to French", + "What is machine learning", + } + + resp, err := c.RewriteText(ctx, t5Gemma2ModelName, inputs) + require.NoError(t, err, "T5Gemma-2 batch rewrite failed") + require.NotNil(t, resp, "Response should not be nil") + require.Len(t, resp.Texts, len(inputs), "Should return output for each input") + + for i, texts := range resp.Texts { + require.NotEmpty(t, texts, "Output %d should have generated text", i) + t.Logf("Input %d: %q -> %q", i, inputs[i], texts[0]) + } +} + +// Helper functions +func fileExists(path string) bool { + _, err := os.Stat(path) + return err == nil +} + +func itoa(i int) string { + return strconv.Itoa(i) +} diff --git a/go.work b/go.work index 8e42ba9..eac45c3 100644 --- a/go.work +++ b/go.work @@ -1,6 +1,7 @@ go 1.25.5 use ( + ../hugot ./e2e ./pkg/client ./pkg/operator @@ -12,6 +13,4 @@ replace github.com/gomlx/gomlx => github.com/timkaye11/gomlx v0.0.0-202512210449 replace github.com/gomlx/onnx-gomlx => github.com/timkaye11/onnx-gomlx v0.0.0-20251217194823-a871bb8cc687 -replace github.com/knights-analytics/hugot => github.com/ajroetker/hugot v0.0.0-20260101005223-9b602a8e2c35 - replace github.com/knights-analytics/ortgenai => github.com/ajroetker/ortgenai v0.0.0-20260102002309-90abcfc27670 diff --git a/openapi.yaml b/openapi.yaml index 8964986..c5db817 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -518,6 +518,70 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + /decode: + post: + summary: Decode text using T5Gemma-2 decoder + description: | + Generate text using the T5Gemma-2 decoder for autoregressive text generation. + This is a novel capability that uses the decoder component of encoder-decoder models. + + ## Models + + - Uses T5Gemma-2 models from `models_dir/rewriters/` that have decoder components + - Requires decoder.onnx and decoder-init.onnx files + + ## Use Cases + + - **Text Continuation**: Generate text continuations from prompts + - **Embedding Inversion**: Reconstruct text from embeddings (experimental) + - **Guided Generation**: Generate text conditioned on encoder outputs + + ## Example + + ```json + { + "model": "google/t5gemma-2-270m", + "prompt": "The meaning of life is" + } + ``` + operationId: decodeText + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DecodeRequest' + responses: + '200': + description: Text decoded successfully + content: + application/json: + schema: + $ref: '#/components/schemas/DecodeResponse' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Model not found or does not support decoding + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '503': + description: Decode service unavailable (no compatible models) + content: + application/json: + schema: + $ref: '#/components/schemas/Error' /models: get: summary: List available models @@ -1007,6 +1071,55 @@ components: items: type: string description: Rewritten texts (array of arrays, one per input, multiple per beam) + DecodeRequest: + type: object + required: + - model + - prompt + properties: + model: + type: string + description: Name of T5Gemma-2 or compatible decoder model + example: google/t5gemma-2-270m + prompt: + type: string + description: Input prompt for text generation + example: "The meaning of life is" + max_tokens: + type: integer + description: Maximum number of tokens to generate (default 256) + default: 256 + temperature: + type: number + format: float + description: Sampling temperature (default 1.0) + default: 1.0 + top_p: + type: number + format: float + description: Nucleus sampling probability (default 1.0) + default: 1.0 + DecodeResponse: + type: object + required: + - model + - texts + properties: + model: + type: string + description: Name of model used for decoding + texts: + type: array + items: + type: string + description: Generated text sequences + tokens: + type: array + items: + type: array + items: + type: integer + description: Token IDs for each generated sequence (optional) ModelsResponse: type: object required: diff --git a/pkg/termite/api.gen.go b/pkg/termite/api.gen.go index 62dd5da..5a5d5ef 100644 --- a/pkg/termite/api.gen.go +++ b/pkg/termite/api.gen.go @@ -641,6 +641,45 @@ type RewriteResponse struct { Texts [][]string `json:"texts"` } +// DecodeRequest defines model for DecodeRequest. +type DecodeRequest struct { + // Model Name of T5Gemma-2 or compatible decoder model + Model string `json:"model"` + + // Prompt Input prompt for text generation (mutually exclusive with Embeddings) + Prompt string `json:"prompt,omitempty"` + + // Embeddings Pre-computed encoder hidden states for embedding inversion + // Shape: [sequence_length, hidden_size] or [batch, sequence_length, hidden_size] + // When provided, the decoder generates text directly from these embeddings + // without running the encoder + Embeddings [][]float32 `json:"embeddings,omitempty"` + + // SequenceLength The sequence length of the embeddings (for reshaping flat arrays) + SequenceLength *int `json:"sequence_length,omitempty"` + + // MaxTokens Maximum number of tokens to generate (default 256) + MaxTokens *int `json:"max_tokens,omitempty"` + + // Temperature Sampling temperature (default 1.0) + Temperature *float32 `json:"temperature,omitempty"` + + // TopP Nucleus sampling probability (default 1.0) + TopP *float32 `json:"top_p,omitempty"` +} + +// DecodeResponse defines model for DecodeResponse. +type DecodeResponse struct { + // Model Name of model used for decoding + Model string `json:"model"` + + // Texts Generated text sequences + Texts []string `json:"texts"` + + // Tokens Token IDs for each generated sequence (optional) + Tokens [][]int `json:"tokens,omitempty"` +} + // Role The role of a message sender in a conversation type Role string @@ -1071,6 +1110,9 @@ type ServerInterface interface { // Rewrite text using Seq2Seq models // (POST /rewrite) RewriteText(w http.ResponseWriter, r *http.Request) + // Decode text using T5Gemma-2 decoder + // (POST /decode) + DecodeText(w http.ResponseWriter, r *http.Request) // Get version information // (GET /version) GetVersion(w http.ResponseWriter, r *http.Request) @@ -1183,6 +1225,20 @@ func (siw *ServerInterfaceWrapper) RewriteText(w http.ResponseWriter, r *http.Re handler.ServeHTTP(w, r) } +// DecodeText operation middleware +func (siw *ServerInterfaceWrapper) DecodeText(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.DecodeText(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // GetVersion operation middleware func (siw *ServerInterfaceWrapper) GetVersion(w http.ResponseWriter, r *http.Request) { @@ -1324,6 +1380,7 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H m.HandleFunc("POST "+options.BaseURL+"/recognize", wrapper.RecognizeEntities) m.HandleFunc("POST "+options.BaseURL+"/rerank", wrapper.RerankPrompts) m.HandleFunc("POST "+options.BaseURL+"/rewrite", wrapper.RewriteText) + m.HandleFunc("POST "+options.BaseURL+"/decode", wrapper.DecodeText) m.HandleFunc("GET "+options.BaseURL+"/version", wrapper.GetVersion) return m diff --git a/pkg/termite/api.go b/pkg/termite/api.go index c3e8f0b..cc3e97c 100644 --- a/pkg/termite/api.go +++ b/pkg/termite/api.go @@ -89,6 +89,11 @@ func (t *TermiteAPI) RewriteText(w http.ResponseWriter, r *http.Request) { t.node.handleApiRewrite(w, r) } +// DecodeText implements ServerInterface - placeholder for future embedding inversion feature +func (t *TermiteAPI) DecodeText(w http.ResponseWriter, r *http.Request) { + http.Error(w, "decode endpoint not yet implemented; use /api/rewrite for text generation", http.StatusNotImplemented) +} + // ListModels implements ServerInterface func (t *TermiteAPI) ListModels(w http.ResponseWriter, r *http.Request) { resp := ModelsResponse{ @@ -1306,6 +1311,9 @@ func (ln *TermiteNode) handleApiRewrite(w http.ResponseWriter, r *http.Request) // Get model from registry model, err := ln.seq2seqRegistry.Get(req.Model) if err != nil { + ln.logger.Error("failed to get rewrite model", + zap.String("model", req.Model), + zap.Error(err)) http.Error(w, fmt.Sprintf("model not found: %s", req.Model), http.StatusNotFound) return } @@ -1339,3 +1347,4 @@ func (ln *TermiteNode) handleApiRewrite(w http.ResponseWriter, r *http.Request) return } } + diff --git a/pkg/termite/embedder_registry.go b/pkg/termite/embedder_registry.go index cf4f393..09bd51e 100644 --- a/pkg/termite/embedder_registry.go +++ b/pkg/termite/embedder_registry.go @@ -194,6 +194,23 @@ func (r *EmbedderRegistry) discoverModels() error { registryFullName := dm.FullName() variants := dm.Variants + // Check if this is a T5Gemma-2 multimodal encoder-decoder model + if isT5Gemma2Model(modelPath) { + r.logger.Info("Discovered T5Gemma-2 embedder model (not loaded)", + zap.String("name", registryFullName), + zap.String("path", modelPath)) + + r.discovered[registryFullName] = &ModelInfo{ + Name: registryFullName, + Path: modelPath, + OnnxFilename: "", // T5Gemma-2 uses multiple files (encoder.onnx, vision_encoder.onnx) + PoolSize: poolSize, + ModelType: "t5gemma2", + Variants: []string{"default"}, + } + continue // Skip standard embedder handling + } + // Check if this is a multimodal (CLIP-style) model hasMultimodalStd, hasMultimodalQt := isMultimodalModel(modelPath) if hasMultimodalStd || hasMultimodalQt { @@ -330,6 +347,14 @@ func (r *EmbedderRegistry) loadModel(info *ModelInfo) (embeddings.Embedder, erro // Handle different model types switch info.ModelType { + case "t5gemma2": + // Load T5Gemma-2 multimodal encoder-decoder model + embedder, backendUsed, err = termembeddings.NewT5Gemma2EmbedderWithSessionManager( + info.Path, + r.sessionManager, + nil, // modelBackends - use default priority (ONNX only) + r.logger.Named(info.Name), + ) case "clip": // Load standard precision CLIP multimodal model embedder, backendUsed, err = termembeddings.NewHugotCLIPEmbedderWithSessionManager( diff --git a/pkg/termite/lib/embeddings/t5gemma2.go b/pkg/termite/lib/embeddings/t5gemma2.go new file mode 100644 index 0000000..ba0de42 --- /dev/null +++ b/pkg/termite/lib/embeddings/t5gemma2.go @@ -0,0 +1,494 @@ +// Copyright 2025 Antfly, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build onnx && ORT + +package embeddings + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "image" + _ "image/gif" + _ "image/jpeg" + _ "image/png" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/antflydb/antfly-go/libaf/ai" + libafembed "github.com/antflydb/antfly-go/libaf/embeddings" + "github.com/antflydb/termite/pkg/termite/lib/hugot" + khugot "github.com/knights-analytics/hugot" + "github.com/knights-analytics/hugot/backends" + "github.com/knights-analytics/hugot/pipelines" + "github.com/knights-analytics/hugot/util/imageutil" + "go.uber.org/zap" + _ "golang.org/x/image/webp" +) + +// T5Gemma2Embedder implements multimodal embeddings using T5Gemma-2 encoder. +// It uses the encoder.onnx for text embeddings and vision_encoder.onnx for images. +// +// T5Gemma-2 is a multimodal encoder-decoder model from Google's Gemma 3 family, +// trained with UL2 objective. The encoder produces rich embeddings that can be +// used for similarity search and retrieval. +// +// The vision encoder is lazily loaded on first image embedding request to save memory. +// +// Build with: CGO_ENABLED=1 go build -tags="onnx,ORT" +type T5Gemma2Embedder struct { + encoderPipeline *pipelines.FeatureExtractionPipeline + visionPipeline *pipelines.FeatureExtractionPipeline + session *khugot.Session + config *T5Gemma2Config + logger *zap.Logger + caps libafembed.EmbedderCapabilities + modelPath string + sessionShared bool + + // Lazy loading for vision encoder + hasVisionFile bool // true if vision_encoder.onnx exists + visionOnce sync.Once // ensures vision pipeline loaded only once + visionLoadErr error // captures any error during lazy load +} + +// T5Gemma2Config holds the T5Gemma-2 model configuration +type T5Gemma2Config struct { + ModelType string `json:"model_type"` + HiddenSize int `json:"hidden_size"` + VocabSize int `json:"vocab_size"` + ImageSize int `json:"image_size"` + ImageSeqLen int `json:"image_seq_length"` // Tokens per image (typically 256) + Capabilities []string `json:"capabilities"` +} + +// NewT5Gemma2Embedder creates a new T5Gemma-2 embedder using hugot pipelines. +// The directory should contain: +// - encoder.onnx (text + multimodal encoder) +// - vision_encoder.onnx (SigLIP vision encoder) +// - config.json +// - tokenizer.json +// +// Build with -tags="onnx,ORT" to enable this embedder. +func NewT5Gemma2Embedder(modelPath string, logger *zap.Logger) (*T5Gemma2Embedder, error) { + return NewT5Gemma2EmbedderWithSession(modelPath, nil, logger) +} + +// NewT5Gemma2EmbedderWithSessionManager creates a new T5Gemma-2 embedder using a SessionManager. +// The SessionManager handles backend selection and session reuse (required for ONNX Runtime which only allows one session). +// Returns the embedder and the backend type that was used. +func NewT5Gemma2EmbedderWithSessionManager(modelPath string, sessionManager *hugot.SessionManager, modelBackends []string, logger *zap.Logger) (*T5Gemma2Embedder, hugot.BackendType, error) { + if sessionManager == nil { + return nil, "", errors.New("sessionManager is required for T5Gemma-2 embedder (ONNX Runtime only allows one session)") + } + + // T5Gemma-2 requires ONNX Runtime backend (not pure Go or XLA) + if modelBackends == nil { + modelBackends = []string{"onnx"} + } + session, backendUsed, err := sessionManager.GetSessionForModel(modelBackends) + if err != nil { + return nil, "", fmt.Errorf("getting session from manager: %w", err) + } + + embedder, err := NewT5Gemma2EmbedderWithSession(modelPath, session, logger) + if err != nil { + return nil, "", err + } + + // SessionManager owns the session, so mark as shared + embedder.sessionShared = true + + return embedder, backendUsed, nil +} + +// NewT5Gemma2EmbedderWithSession creates a new T5Gemma-2 embedder using an optional shared session. +func NewT5Gemma2EmbedderWithSession(modelPath string, sharedSession *khugot.Session, logger *zap.Logger) (*T5Gemma2Embedder, error) { + if modelPath == "" { + return nil, errors.New("model path is required") + } + + if logger == nil { + logger = zap.NewNop() + } + + logger.Info("Initializing T5Gemma-2 embedder", + zap.String("modelPath", modelPath), + zap.String("backend", hugot.BackendName())) + + // Load configuration + config, err := loadT5Gemma2Config(modelPath) + if err != nil { + return nil, fmt.Errorf("loading T5Gemma-2 config: %w", err) + } + + // Verify required files exist + encoderPath := filepath.Join(modelPath, "encoder.onnx") + if _, err := os.Stat(encoderPath); err != nil { + return nil, fmt.Errorf("encoder model not found: %s", encoderPath) + } + + // Check for optional vision encoder + visionPath := filepath.Join(modelPath, "vision_encoder.onnx") + hasVision := false + if _, err := os.Stat(visionPath); err == nil { + hasVision = true + } + + // Create or reuse session + session, err := hugot.NewSessionOrUseExisting(sharedSession) + if err != nil { + return nil, fmt.Errorf("creating hugot session: %w", err) + } + sessionShared := (sharedSession != nil) + + // Create text encoder pipeline first (always needed) + encoderPipelineName := fmt.Sprintf("%s:encoder:encoder.onnx", modelPath) + encoderConfig := khugot.FeatureExtractionConfig{ + ModelPath: modelPath, + Name: encoderPipelineName, + OnnxFilename: "encoder.onnx", + Options: []backends.PipelineOption[*pipelines.FeatureExtractionPipeline]{ + pipelines.WithNormalization(), + }, + } + + encoderPipeline, err := khugot.NewPipeline(session, encoderConfig) + if err != nil { + if !sessionShared { + _ = session.Destroy() + } + return nil, fmt.Errorf("creating encoder pipeline: %w", err) + } + + // Vision pipeline is loaded lazily on first image embed request to save memory. + // We only check if the file exists here; actual loading happens in loadVisionPipeline(). + if hasVision { + logger.Info("T5Gemma-2 embedder initialized (vision available, will load on first use)", + zap.Int("hiddenSize", config.HiddenSize), + zap.Int("imageSize", config.ImageSize)) + } else { + logger.Info("T5Gemma-2 embedder initialized (text-only, no vision encoder file)", + zap.Int("hiddenSize", config.HiddenSize)) + } + + // Build capabilities based on available features + mimeTypes := []libafembed.MIMETypeSupport{ + {MIMEType: "text/plain"}, + } + if hasVision { + mimeTypes = append(mimeTypes, + libafembed.MIMETypeSupport{MIMEType: "image/png"}, + libafembed.MIMETypeSupport{MIMEType: "image/jpeg"}, + libafembed.MIMETypeSupport{MIMEType: "image/gif"}, + libafembed.MIMETypeSupport{MIMEType: "image/webp"}, + ) + } + + return &T5Gemma2Embedder{ + encoderPipeline: encoderPipeline, + visionPipeline: nil, // Loaded lazily + session: session, + config: config, + logger: logger, + modelPath: modelPath, + sessionShared: sessionShared, + hasVisionFile: hasVision, + caps: libafembed.EmbedderCapabilities{ + SupportedMIMETypes: mimeTypes, + Dimensions: []int{config.HiddenSize}, + DefaultDimension: config.HiddenSize, + SupportsFusion: false, + }, + }, nil +} + +// Capabilities returns the embedder capabilities +func (t *T5Gemma2Embedder) Capabilities() libafembed.EmbedderCapabilities { + return t.caps +} + +// Embed generates embeddings for the given content. +// For text content, uses the text encoder. +// For image content (BinaryContent), uses the vision encoder. +// For mixed content, processes each modality appropriately. +func (t *T5Gemma2Embedder) Embed(ctx context.Context, contents [][]ai.ContentPart) ([][]float32, error) { + if len(contents) == 0 { + return [][]float32{}, nil + } + + embeddings := make([][]float32, len(contents)) + + for i, parts := range contents { + var embedding []float32 + var err error + + for _, part := range parts { + switch p := part.(type) { + case ai.BinaryContent: + if strings.HasPrefix(p.MIMEType, "image/") { + embedding, err = t.embedImage(p.Data) + if err != nil { + return nil, fmt.Errorf("embedding image at index %d: %w", i, err) + } + } + case ai.TextContent: + embedding, err = t.embedText(p.Text) + if err != nil { + return nil, fmt.Errorf("embedding text at index %d: %w", i, err) + } + } + + if embedding != nil { + break + } + } + + if embedding == nil { + return nil, fmt.Errorf("no valid content found at index %d", i) + } + + embeddings[i] = embedding + } + + return embeddings, nil +} + +// loadVisionPipeline lazily loads the vision encoder on first use. +// This saves memory when only text embeddings are needed. +func (t *T5Gemma2Embedder) loadVisionPipeline() error { + t.visionOnce.Do(func() { + if !t.hasVisionFile { + t.visionLoadErr = errors.New("vision encoder not available: vision_encoder.onnx not found") + return + } + + t.logger.Info("Lazily loading vision encoder pipeline", + zap.String("modelPath", t.modelPath)) + + imageSize := t.config.ImageSize + if imageSize == 0 { + imageSize = 896 // Default for T5Gemma-2 (SigLIP) + } + + visionPipelineName := fmt.Sprintf("%s:vision:vision_encoder.onnx", t.modelPath) + visionConfig := khugot.FeatureExtractionConfig{ + ModelPath: t.modelPath, + Name: visionPipelineName, + OnnxFilename: "vision_encoder.onnx", + Options: []backends.PipelineOption[*pipelines.FeatureExtractionPipeline]{ + pipelines.WithImageMode(), + pipelines.WithPreprocessSteps[*pipelines.FeatureExtractionPipeline]( + imageutil.ResizeStep(imageSize), + imageutil.CenterCropStep(imageSize, imageSize), + ), + pipelines.WithNormalizationSteps[*pipelines.FeatureExtractionPipeline]( + imageutil.RescaleStep(), + imageutil.CLIPPixelNormalizationStep(), + ), + pipelines.WithNCHWFormat[*pipelines.FeatureExtractionPipeline](), + pipelines.WithNormalization(), + }, + } + + pipeline, err := khugot.NewPipeline(t.session, visionConfig) + if err != nil { + t.visionLoadErr = fmt.Errorf("loading vision pipeline: %w", err) + t.logger.Error("Failed to load vision encoder", zap.Error(err)) + return + } + + t.visionPipeline = pipeline + t.logger.Info("Vision encoder loaded successfully", + zap.Int("imageSize", imageSize)) + }) + + return t.visionLoadErr +} + +// embedImage processes an image and returns its embedding using the vision pipeline. +// The vision pipeline is lazily loaded on first call to save memory. +func (t *T5Gemma2Embedder) embedImage(imageData []byte) ([]float32, error) { + // Lazy load vision pipeline + if err := t.loadVisionPipeline(); err != nil { + return nil, err + } + + // Decode image + img, _, err := image.Decode(bytes.NewReader(imageData)) + if err != nil { + return nil, fmt.Errorf("decoding image: %w", err) + } + + // Run through vision pipeline + output, err := t.visionPipeline.RunWithImages([]image.Image{img}) + if err != nil { + return nil, fmt.Errorf("running vision pipeline: %w", err) + } + + if len(output.Embeddings) == 0 || len(output.Embeddings[0]) == 0 { + return nil, errors.New("no embedding returned from vision pipeline") + } + + // Pipeline already normalizes if WithNormalization() was used + return output.Embeddings[0], nil +} + +// HasVision returns true if this embedder supports image embeddings. +// Note: This returns true if vision_encoder.onnx exists, even if not yet loaded. +func (t *T5Gemma2Embedder) HasVision() bool { + return t.hasVisionFile +} + +// IsVisionLoaded returns true if the vision pipeline has been loaded. +func (t *T5Gemma2Embedder) IsVisionLoaded() bool { + return t.visionPipeline != nil +} + +// embedText tokenizes text and returns its embedding using the encoder pipeline +func (t *T5Gemma2Embedder) embedText(text string) ([]float32, error) { + // Run through encoder pipeline + output, err := t.encoderPipeline.RunPipeline([]string{text}) + if err != nil { + return nil, fmt.Errorf("running encoder pipeline: %w", err) + } + + if len(output.Embeddings) == 0 || len(output.Embeddings[0]) == 0 { + return nil, errors.New("no embedding returned from encoder pipeline") + } + + // Pipeline already normalizes if WithNormalization() was used + return output.Embeddings[0], nil +} + +// Close releases resources +func (t *T5Gemma2Embedder) Close() error { + if t.session != nil && !t.sessionShared { + t.logger.Info("Destroying Hugot session (owned by this T5Gemma-2 embedder)") + return t.session.Destroy() + } else if t.sessionShared { + t.logger.Debug("Skipping session destruction (shared session)") + } + return nil +} + +// loadT5Gemma2Config loads T5Gemma-2 configuration from model directory +func loadT5Gemma2Config(modelPath string) (*T5Gemma2Config, error) { + // Try t5gemma2_config.json first (our custom config), then config.json + configPaths := []string{ + filepath.Join(modelPath, "t5gemma2_config.json"), + filepath.Join(modelPath, "config.json"), + } + + for _, path := range configPaths { + data, err := os.ReadFile(path) + if err != nil { + continue + } + + // Parse config + var rawConfig struct { + ModelType string `json:"model_type"` + HiddenSize int `json:"hidden_size"` + VocabSize int `json:"vocab_size"` + Capabilities []string `json:"capabilities"` + Encoder struct { + VisionConfig struct { + ImageSize int `json:"image_size"` + } `json:"vision_config"` + MMTokensPerImage int `json:"mm_tokens_per_image"` + } `json:"encoder"` + Decoder struct { + HiddenSize int `json:"hidden_size"` + } `json:"decoder"` + } + + if err := json.Unmarshal(data, &rawConfig); err != nil { + continue + } + + // Extract values with fallbacks + hiddenSize := rawConfig.HiddenSize + if hiddenSize == 0 { + hiddenSize = rawConfig.Decoder.HiddenSize + } + if hiddenSize == 0 { + hiddenSize = 640 // Default for T5Gemma-2 270M + } + + imageSize := rawConfig.Encoder.VisionConfig.ImageSize + if imageSize == 0 { + imageSize = 896 // Default for SigLIP in T5Gemma-2 + } + + imageSeqLen := rawConfig.Encoder.MMTokensPerImage + if imageSeqLen == 0 { + imageSeqLen = 256 // Default + } + + return &T5Gemma2Config{ + ModelType: rawConfig.ModelType, + HiddenSize: hiddenSize, + VocabSize: rawConfig.VocabSize, + ImageSize: imageSize, + ImageSeqLen: imageSeqLen, + Capabilities: rawConfig.Capabilities, + }, nil + } + + // Return default config for T5Gemma-2 270M + return &T5Gemma2Config{ + ModelType: "t5gemma2", + HiddenSize: 640, + VocabSize: 262144, + ImageSize: 896, + ImageSeqLen: 256, + Capabilities: []string{"embeddings", "generation", "decoding", "multimodal"}, + }, nil +} + +// IsT5Gemma2Model checks if a model directory contains T5Gemma-2 model files. +// Only encoder.onnx is required; vision_encoder.onnx is optional for text-only use. +func IsT5Gemma2Model(modelPath string) bool { + encoderPath := filepath.Join(modelPath, "encoder.onnx") + + // Must have encoder (vision encoder is optional) + if _, err := os.Stat(encoderPath); err != nil { + return false + } + + // Check config for model_type + configPath := filepath.Join(modelPath, "config.json") + if data, err := os.ReadFile(configPath); err == nil { + var config struct { + ModelType string `json:"model_type"` + } + if err := json.Unmarshal(data, &config); err == nil { + return config.ModelType == "t5gemma2" + } + } + + // Also check t5gemma2_config.json + t5gemma2ConfigPath := filepath.Join(modelPath, "t5gemma2_config.json") + if _, err := os.Stat(t5gemma2ConfigPath); err == nil { + return true + } + + return false +} diff --git a/pkg/termite/lib/embeddings/t5gemma2_stub.go b/pkg/termite/lib/embeddings/t5gemma2_stub.go new file mode 100644 index 0000000..669c2aa --- /dev/null +++ b/pkg/termite/lib/embeddings/t5gemma2_stub.go @@ -0,0 +1,97 @@ +// Copyright 2025 Antfly, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !(onnx && ORT) + +package embeddings + +import ( + "context" + "encoding/json" + "errors" + "os" + "path/filepath" + + "github.com/antflydb/antfly-go/libaf/ai" + libafembed "github.com/antflydb/antfly-go/libaf/embeddings" + "github.com/antflydb/termite/pkg/termite/lib/hugot" + "go.uber.org/zap" +) + +// T5Gemma2Embedder is a stub when built without ONNX support. +// To enable T5Gemma-2 multimodal embeddings, build with: CGO_ENABLED=1 go build -tags="onnx,ORT" +type T5Gemma2Embedder struct{} + +// NewT5Gemma2Embedder returns an error when T5Gemma-2 support is disabled. +func NewT5Gemma2Embedder(modelPath string, logger *zap.Logger) (*T5Gemma2Embedder, error) { + return nil, errors.New("T5Gemma-2 embedder not available: build with -tags=\"onnx,ORT\" to enable") +} + +// NewT5Gemma2EmbedderWithSession returns an error when T5Gemma-2 support is disabled. +func NewT5Gemma2EmbedderWithSession(modelPath string, sharedSession interface{}, logger *zap.Logger) (*T5Gemma2Embedder, error) { + return nil, errors.New("T5Gemma-2 embedder not available: build with -tags=\"onnx,ORT\" to enable") +} + +// NewT5Gemma2EmbedderWithSessionManager returns an error when T5Gemma-2 support is disabled. +func NewT5Gemma2EmbedderWithSessionManager(modelPath string, sessionManager *hugot.SessionManager, modelBackends []string, logger *zap.Logger) (*T5Gemma2Embedder, hugot.BackendType, error) { + return nil, "", errors.New("T5Gemma-2 embedder not available: build with -tags=\"onnx,ORT\" to enable") +} + +// Capabilities returns empty capabilities for the stub. +func (t *T5Gemma2Embedder) Capabilities() libafembed.EmbedderCapabilities { + return libafembed.EmbedderCapabilities{} +} + +// Embed returns an error for the stub since it cannot be used. +func (t *T5Gemma2Embedder) Embed(ctx context.Context, contents [][]ai.ContentPart) ([][]float32, error) { + return nil, errors.New("T5Gemma-2 embedder not available: build with -tags=\"onnx,ORT\" to enable") +} + +// Close is a no-op for the stub. +func (t *T5Gemma2Embedder) Close() error { + return nil +} + +// IsT5Gemma2Model checks if a model directory contains T5Gemma-2 model files +func IsT5Gemma2Model(modelPath string) bool { + encoderPath := filepath.Join(modelPath, "encoder.onnx") + visionPath := filepath.Join(modelPath, "vision_encoder.onnx") + + // Must have both encoder and vision encoder + if _, err := os.Stat(encoderPath); err != nil { + return false + } + if _, err := os.Stat(visionPath); err != nil { + return false + } + + // Check config for model_type + configPath := filepath.Join(modelPath, "config.json") + if data, err := os.ReadFile(configPath); err == nil { + var config struct { + ModelType string `json:"model_type"` + } + if err := json.Unmarshal(data, &config); err == nil { + return config.ModelType == "t5gemma2" + } + } + + // Also check t5gemma2_config.json + t5gemma2ConfigPath := filepath.Join(modelPath, "t5gemma2_config.json") + if _, err := os.Stat(t5gemma2ConfigPath); err == nil { + return true + } + + return false +} diff --git a/pkg/termite/lib/seq2seq/t5gemma2.go b/pkg/termite/lib/seq2seq/t5gemma2.go new file mode 100644 index 0000000..4205a45 --- /dev/null +++ b/pkg/termite/lib/seq2seq/t5gemma2.go @@ -0,0 +1,573 @@ +// Copyright 2025 Antfly, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build onnx && ORT + +package seq2seq + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "image" + _ "image/gif" + _ "image/jpeg" + _ "image/png" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/antflydb/termite/pkg/termite/lib/hugot" + khugot "github.com/knights-analytics/hugot" + "github.com/knights-analytics/hugot/backends" + "github.com/knights-analytics/hugot/pipelines" + "github.com/knights-analytics/hugot/util/imageutil" + "go.uber.org/zap" + _ "golang.org/x/image/webp" +) + +// Ensure T5Gemma2Generator implements the Model interface +var _ Model = (*T5Gemma2Generator)(nil) + +// T5Gemma2Generator implements multimodal seq2seq text generation using T5Gemma-2. +// It supports text-to-text and image+text-to-text generation. +// +// T5Gemma-2 is a multimodal encoder-decoder model from Google's Gemma 3 family, +// trained with UL2 objective. It can generate text conditioned on both text and images. +// +// Build with: CGO_ENABLED=1 go build -tags="onnx,ORT" +type T5Gemma2Generator struct { + session *khugot.Session + seq2seqPipeline *pipelines.Seq2SeqPipeline + visionPipeline *pipelines.FeatureExtractionPipeline + logger *zap.Logger + sessionShared bool + config T5Gemma2GeneratorConfig + modelPath string + + // Lazy loading for vision encoder + hasVisionFile bool // true if vision_encoder.onnx exists + visionOnce sync.Once // ensures vision pipeline loaded only once + visionLoadErr error // captures any error during lazy load +} + +// T5Gemma2GeneratorConfig holds configuration for T5Gemma-2 generation +type T5Gemma2GeneratorConfig struct { + // ModelID is the original HuggingFace model ID + ModelID string `json:"model_id"` + // ModelType should be "t5gemma2" + ModelType string `json:"model_type"` + // Task indicates the model's intended use + Task string `json:"task"` + // MaxNewTokens is the maximum number of tokens to generate + MaxNewTokens int `json:"max_new_tokens"` + // NumBeams is the number of beams for beam search (1 = greedy) + NumBeams int `json:"num_beams"` + // DoSample enables sampling instead of greedy/beam search + DoSample bool `json:"do_sample"` + // Temperature controls randomness (used when DoSample=true) + Temperature float32 `json:"temperature"` + // TopP is the nucleus sampling probability (used when DoSample=true) + TopP float32 `json:"top_p"` + // ImageSize for vision encoder preprocessing + ImageSize int `json:"image_size"` + // HiddenSize of the model + HiddenSize int `json:"hidden_size"` +} + +// MultimodalInput represents input that can contain both text and images +type MultimodalInput struct { + // Text is the input text prompt + Text string + // Images contains base64-encoded or raw image data + Images [][]byte +} + +// NewT5Gemma2Generator creates a new T5Gemma-2 generator. +// The directory should contain: +// - encoder.onnx (text + multimodal encoder) +// - decoder.onnx (with past_key_values) +// - decoder-init.onnx (without past_key_values) +// - vision_encoder.onnx (SigLIP vision encoder) +// - config.json or t5gemma2_config.json +// - tokenizer.json +// +// Build with -tags="onnx,ORT" to enable this generator. +func NewT5Gemma2Generator(modelPath string, logger *zap.Logger) (*T5Gemma2Generator, error) { + return NewT5Gemma2GeneratorWithSession(modelPath, nil, logger) +} + +// NewT5Gemma2GeneratorWithSession creates a new T5Gemma-2 generator with an optional shared session. +func NewT5Gemma2GeneratorWithSession(modelPath string, sharedSession *khugot.Session, logger *zap.Logger) (*T5Gemma2Generator, error) { + if modelPath == "" { + return nil, errors.New("model path is required") + } + + if logger == nil { + logger = zap.NewNop() + } + + logger.Info("Initializing T5Gemma-2 generator", + zap.String("modelPath", modelPath), + zap.String("backend", hugot.BackendName())) + + // Load configuration + config, err := loadT5Gemma2GeneratorConfig(modelPath) + if err != nil { + return nil, fmt.Errorf("loading T5Gemma-2 config: %w", err) + } + + // Verify required files exist + requiredFiles := []string{"encoder.onnx", "decoder.onnx", "decoder-init.onnx"} + for _, file := range requiredFiles { + filePath := filepath.Join(modelPath, file) + if _, err := os.Stat(filePath); err != nil { + return nil, fmt.Errorf("required file not found: %s", filePath) + } + } + + // Check for optional vision encoder + visionPath := filepath.Join(modelPath, "vision_encoder.onnx") + hasVision := false + if _, err := os.Stat(visionPath); err == nil { + hasVision = true + } + + // Create or reuse session + session, err := hugot.NewSessionOrUseExisting(sharedSession) + if err != nil { + return nil, fmt.Errorf("creating hugot session: %w", err) + } + sessionShared := (sharedSession != nil) + + // Create Seq2Seq pipeline for text generation + pipelineName := fmt.Sprintf("t5gemma2-seq2seq:%s", filepath.Base(modelPath)) + pipelineOptions := []khugot.Seq2SeqOption{ + pipelines.WithSeq2SeqMaxTokens(config.MaxNewTokens), + } + + if config.DoSample && config.TopP > 0 && config.Temperature > 0 { + pipelineOptions = append(pipelineOptions, + pipelines.WithSampling(config.TopP, config.Temperature)) + } + + pipelineConfig := khugot.Seq2SeqConfig{ + ModelPath: modelPath, + Name: pipelineName, + Options: pipelineOptions, + } + + seq2seqPipeline, err := khugot.NewPipeline(session, pipelineConfig) + if err != nil { + if !sessionShared { + session.Destroy() + } + return nil, fmt.Errorf("creating Seq2Seq pipeline: %w", err) + } + + // Vision pipeline is loaded lazily on first image request to save memory. + // Only log whether the file exists, don't load it yet. + if hasVision { + logger.Info("T5Gemma-2 generator initialized (vision available, will load on first use)", + zap.String("task", config.Task), + zap.Int("max_new_tokens", config.MaxNewTokens), + zap.Int("imageSize", config.ImageSize)) + } else { + logger.Info("T5Gemma-2 generator initialized (text-only mode)", + zap.String("task", config.Task), + zap.Int("max_new_tokens", config.MaxNewTokens)) + } + + return &T5Gemma2Generator{ + session: session, + seq2seqPipeline: seq2seqPipeline, + visionPipeline: nil, // Loaded lazily + logger: logger, + sessionShared: sessionShared, + config: config, + modelPath: modelPath, + hasVisionFile: hasVision, + }, nil +} + +// NewT5Gemma2GeneratorWithSessionManager creates a new T5Gemma-2 generator using a SessionManager. +func NewT5Gemma2GeneratorWithSessionManager(modelPath string, sessionManager *hugot.SessionManager, modelBackends []string, logger *zap.Logger) (*T5Gemma2Generator, hugot.BackendType, error) { + if sessionManager == nil { + gen, err := NewT5Gemma2GeneratorWithSession(modelPath, nil, logger) + if err != nil { + return nil, "", err + } + return gen, hugot.BackendType(""), nil + } + + // T5Gemma-2 requires ONNX Runtime + if modelBackends == nil { + modelBackends = []string{"onnx"} + } + session, backendUsed, err := sessionManager.GetSessionForModel(modelBackends) + if err != nil { + return nil, "", fmt.Errorf("getting session from manager: %w", err) + } + + gen, err := NewT5Gemma2GeneratorWithSession(modelPath, session, logger) + if err != nil { + return nil, "", err + } + + gen.sessionShared = true + return gen, backendUsed, nil +} + +// Generate runs text generation on the given text inputs. +func (g *T5Gemma2Generator) Generate(ctx context.Context, inputs []string) (*GeneratedOutput, error) { + if len(inputs) == 0 { + return &GeneratedOutput{ + Texts: [][]string{}, + Tokens: [][][]uint32{}, + }, nil + } + + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + g.logger.Debug("Starting T5Gemma-2 generation", + zap.Int("num_inputs", len(inputs))) + + output, err := g.seq2seqPipeline.RunPipeline(inputs) + if err != nil { + g.logger.Error("T5Gemma-2 generation failed", zap.Error(err)) + return nil, fmt.Errorf("running T5Gemma-2 pipeline: %w", err) + } + + g.logger.Debug("T5Gemma-2 generation completed", + zap.Int("num_inputs", len(inputs)), + zap.Int("total_outputs", len(output.GeneratedTexts))) + + return &GeneratedOutput{ + Texts: output.GeneratedTexts, + Tokens: output.GeneratedTokens, + }, nil +} + +// GenerateMultimodal generates text from multimodal inputs (text + images). +// +// CURRENT LIMITATION: This method validates and processes images through the vision +// encoder but does not yet inject the image embeddings into the text generation. +// The seq2seq pipeline currently only processes the text with placeholders. +// Full multimodal generation (where image embeddings condition the output) requires +// modifications to the hugot Seq2SeqPipeline to accept external embeddings. +// +// Use ProcessImage() to get image embeddings for custom multimodal workflows. +func (g *T5Gemma2Generator) GenerateMultimodal(ctx context.Context, inputs []MultimodalInput) (*GeneratedOutput, error) { + if len(inputs) == 0 { + return &GeneratedOutput{ + Texts: [][]string{}, + Tokens: [][][]uint32{}, + }, nil + } + + // Load vision pipeline lazily when we have images to process + hasImages := false + for _, input := range inputs { + if len(input.Images) > 0 { + hasImages = true + break + } + } + + if hasImages { + if err := g.loadVisionPipeline(); err != nil { + return nil, fmt.Errorf("multimodal generation: %w", err) + } + } + + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + // Validate and process images through the vision pipeline + // This ensures images are valid even though embeddings aren't used yet + for i, input := range inputs { + for j, imgData := range input.Images { + if _, err := g.ProcessImage(imgData); err != nil { + return nil, fmt.Errorf("processing image %d for input %d: %w", j, i, err) + } + } + } + + // Process text inputs with image placeholders + // Note: Image embeddings are validated above but not injected into generation yet + textInputs := make([]string, len(inputs)) + for i, input := range inputs { + // Prepend image tokens for each image + prefix := "" + for range input.Images { + prefix += " " + } + textInputs[i] = prefix + input.Text + } + + return g.Generate(ctx, textInputs) +} + +// ProcessImage encodes an image through the vision encoder. +// Returns the image embeddings that can be used for conditioning. +// The vision encoder is loaded lazily on first call. +func (g *T5Gemma2Generator) ProcessImage(imageData []byte) ([]float32, error) { + if err := g.loadVisionPipeline(); err != nil { + return nil, err + } + + // Decode image + img, _, err := image.Decode(bytes.NewReader(imageData)) + if err != nil { + return nil, fmt.Errorf("decoding image: %w", err) + } + + // Run through vision pipeline + output, err := g.visionPipeline.RunWithImages([]image.Image{img}) + if err != nil { + return nil, fmt.Errorf("running vision pipeline: %w", err) + } + + if len(output.Embeddings) == 0 || len(output.Embeddings[0]) == 0 { + return nil, errors.New("no embedding returned from vision pipeline") + } + + return output.Embeddings[0], nil +} + +// HasVision returns true if this generator supports image input. +// This checks if the vision encoder file exists, not if it's currently loaded. +func (g *T5Gemma2Generator) HasVision() bool { + return g.hasVisionFile +} + +// IsVisionLoaded returns true if the vision encoder is currently loaded in memory. +func (g *T5Gemma2Generator) IsVisionLoaded() bool { + return g.visionPipeline != nil +} + +// loadVisionPipeline lazily loads the vision encoder pipeline. +// Thread-safe: uses sync.Once to ensure only one load attempt. +func (g *T5Gemma2Generator) loadVisionPipeline() error { + g.visionOnce.Do(func() { + if !g.hasVisionFile { + g.visionLoadErr = errors.New("vision encoder not available: vision_encoder.onnx not found") + return + } + + g.logger.Info("Lazily loading vision encoder pipeline", + zap.String("modelPath", g.modelPath)) + + imageSize := g.config.ImageSize + if imageSize == 0 { + imageSize = 896 // Default for T5Gemma-2 (SigLIP) + } + + visionPipelineName := fmt.Sprintf("t5gemma2-vision:%s", filepath.Base(g.modelPath)) + visionConfig := khugot.FeatureExtractionConfig{ + ModelPath: g.modelPath, + Name: visionPipelineName, + OnnxFilename: "vision_encoder.onnx", + Options: []backends.PipelineOption[*pipelines.FeatureExtractionPipeline]{ + pipelines.WithImageMode(), + pipelines.WithPreprocessSteps[*pipelines.FeatureExtractionPipeline]( + imageutil.ResizeStep(imageSize), + imageutil.CenterCropStep(imageSize, imageSize), + ), + pipelines.WithNormalizationSteps[*pipelines.FeatureExtractionPipeline]( + imageutil.RescaleStep(), + imageutil.CLIPPixelNormalizationStep(), + ), + pipelines.WithNCHWFormat[*pipelines.FeatureExtractionPipeline](), + }, + } + + pipeline, err := khugot.NewPipeline(g.session, visionConfig) + if err != nil { + g.visionLoadErr = fmt.Errorf("loading vision pipeline: %w", err) + return + } + + g.visionPipeline = pipeline + g.logger.Info("Vision encoder pipeline loaded successfully", + zap.Int("imageSize", imageSize)) + }) + return g.visionLoadErr +} + +// Config returns the generator configuration. +func (g *T5Gemma2Generator) Config() T5Gemma2GeneratorConfig { + return g.config +} + +// Close releases resources. +func (g *T5Gemma2Generator) Close() error { + var errs []error + + if g.seq2seqPipeline != nil { + if err := g.seq2seqPipeline.Destroy(); err != nil { + errs = append(errs, fmt.Errorf("destroying seq2seq pipeline: %w", err)) + } + } + + // Note: FeatureExtractionPipeline doesn't have a Destroy method, + // it's cleaned up when the session is destroyed + + if g.session != nil && !g.sessionShared { + g.logger.Info("Destroying Hugot session (owned by this T5Gemma-2 generator)") + g.session.Destroy() + } + + return errors.Join(errs...) +} + +// loadT5Gemma2GeneratorConfig loads T5Gemma-2 generation config from model directory +func loadT5Gemma2GeneratorConfig(modelPath string) (T5Gemma2GeneratorConfig, error) { + config := T5Gemma2GeneratorConfig{ + MaxNewTokens: 256, + NumBeams: 1, + DoSample: false, + Temperature: 1.0, + TopP: 1.0, + ImageSize: 896, + HiddenSize: 640, + } + + // Try t5gemma2_config.json first, then config.json + configPaths := []string{ + filepath.Join(modelPath, "t5gemma2_config.json"), + filepath.Join(modelPath, "config.json"), + } + + for _, path := range configPaths { + data, err := os.ReadFile(path) + if err != nil { + continue + } + + // Parse config with nested generation_config + var rawConfig struct { + ModelID string `json:"model_id"` + ModelType string `json:"model_type"` + Task string `json:"task"` + HiddenSize int `json:"hidden_size"` + GenerationConfig struct { + MaxNewTokens int `json:"max_new_tokens"` + NumBeams int `json:"num_beams"` + DoSample bool `json:"do_sample"` + Temperature float32 `json:"temperature"` + TopP float32 `json:"top_p"` + } `json:"generation_config"` + Encoder struct { + VisionConfig struct { + ImageSize int `json:"image_size"` + } `json:"vision_config"` + } `json:"encoder"` + Decoder struct { + HiddenSize int `json:"hidden_size"` + } `json:"decoder"` + } + + if err := json.Unmarshal(data, &rawConfig); err != nil { + continue + } + + config.ModelID = rawConfig.ModelID + config.ModelType = rawConfig.ModelType + config.Task = rawConfig.Task + + if rawConfig.GenerationConfig.MaxNewTokens > 0 { + config.MaxNewTokens = rawConfig.GenerationConfig.MaxNewTokens + } + if rawConfig.GenerationConfig.NumBeams > 0 { + config.NumBeams = rawConfig.GenerationConfig.NumBeams + } + config.DoSample = rawConfig.GenerationConfig.DoSample + if rawConfig.GenerationConfig.Temperature > 0 { + config.Temperature = rawConfig.GenerationConfig.Temperature + } + if rawConfig.GenerationConfig.TopP > 0 { + config.TopP = rawConfig.GenerationConfig.TopP + } + + if rawConfig.Encoder.VisionConfig.ImageSize > 0 { + config.ImageSize = rawConfig.Encoder.VisionConfig.ImageSize + } + if rawConfig.HiddenSize > 0 { + config.HiddenSize = rawConfig.HiddenSize + } else if rawConfig.Decoder.HiddenSize > 0 { + config.HiddenSize = rawConfig.Decoder.HiddenSize + } + + return config, nil + } + + return config, nil +} + +// IsT5Gemma2GeneratorModel checks if a model directory contains T5Gemma-2 generator files. +func IsT5Gemma2GeneratorModel(modelPath string) bool { + // Must have standard seq2seq files + requiredFiles := []string{"encoder.onnx", "decoder.onnx", "decoder-init.onnx"} + for _, file := range requiredFiles { + filePath := filepath.Join(modelPath, file) + if _, err := os.Stat(filePath); err != nil { + return false + } + } + + // Check config for model_type: "t5gemma2" + configPath := filepath.Join(modelPath, "config.json") + if data, err := os.ReadFile(configPath); err == nil { + var config struct { + ModelType string `json:"model_type"` + } + if err := json.Unmarshal(data, &config); err == nil { + return config.ModelType == "t5gemma2" + } + } + + // Also check t5gemma2_config.json + t5gemma2ConfigPath := filepath.Join(modelPath, "t5gemma2_config.json") + if _, err := os.Stat(t5gemma2ConfigPath); err == nil { + return true + } + + return false +} + +// DecodeBase64Image decodes a base64-encoded image (data URI or raw base64). +func DecodeBase64Image(input string) ([]byte, error) { + // Handle data URI format: data:image/png;base64,... + if strings.HasPrefix(input, "data:") { + parts := strings.SplitN(input, ",", 2) + if len(parts) != 2 { + return nil, errors.New("invalid data URI format") + } + input = parts[1] + } + + return base64.StdEncoding.DecodeString(input) +} diff --git a/pkg/termite/lib/seq2seq/t5gemma2_stub.go b/pkg/termite/lib/seq2seq/t5gemma2_stub.go new file mode 100644 index 0000000..38c8a18 --- /dev/null +++ b/pkg/termite/lib/seq2seq/t5gemma2_stub.go @@ -0,0 +1,143 @@ +// Copyright 2025 Antfly, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !(onnx && ORT) + +package seq2seq + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "os" + "path/filepath" + "strings" + + "github.com/antflydb/termite/pkg/termite/lib/hugot" + "go.uber.org/zap" +) + +// T5Gemma2Generator is a stub when built without ONNX support. +// To enable T5Gemma-2 generation, build with: CGO_ENABLED=1 go build -tags="onnx,ORT" +type T5Gemma2Generator struct{} + +// T5Gemma2GeneratorConfig holds configuration for T5Gemma-2 generation +type T5Gemma2GeneratorConfig struct { + ModelID string `json:"model_id"` + ModelType string `json:"model_type"` + Task string `json:"task"` + MaxNewTokens int `json:"max_new_tokens"` + NumBeams int `json:"num_beams"` + DoSample bool `json:"do_sample"` + Temperature float32 `json:"temperature"` + TopP float32 `json:"top_p"` + ImageSize int `json:"image_size"` + HiddenSize int `json:"hidden_size"` +} + +// MultimodalInput represents input that can contain both text and images +type MultimodalInput struct { + Text string + Images [][]byte +} + +// NewT5Gemma2Generator returns an error when ONNX Runtime is not available. +func NewT5Gemma2Generator(modelPath string, logger *zap.Logger) (*T5Gemma2Generator, error) { + return nil, errors.New("T5Gemma-2 generator requires ONNX Runtime: build with -tags=\"onnx,ORT\"") +} + +// NewT5Gemma2GeneratorWithSession returns an error when ONNX Runtime is not available. +func NewT5Gemma2GeneratorWithSession(modelPath string, sharedSession interface{}, logger *zap.Logger) (*T5Gemma2Generator, error) { + return nil, errors.New("T5Gemma-2 generator requires ONNX Runtime: build with -tags=\"onnx,ORT\"") +} + +// NewT5Gemma2GeneratorWithSessionManager returns an error when ONNX Runtime is not available. +func NewT5Gemma2GeneratorWithSessionManager(modelPath string, sessionManager *hugot.SessionManager, modelBackends []string, logger *zap.Logger) (*T5Gemma2Generator, hugot.BackendType, error) { + return nil, "", errors.New("T5Gemma-2 generator requires ONNX Runtime: build with -tags=\"onnx,ORT\"") +} + +// Generate returns an error for the stub. +func (g *T5Gemma2Generator) Generate(ctx context.Context, inputs []string) (*GeneratedOutput, error) { + return nil, errors.New("T5Gemma-2 generator requires ONNX Runtime: build with -tags=\"onnx,ORT\"") +} + +// GenerateMultimodal returns an error for the stub. +func (g *T5Gemma2Generator) GenerateMultimodal(ctx context.Context, inputs []MultimodalInput) (*GeneratedOutput, error) { + return nil, errors.New("T5Gemma-2 generator requires ONNX Runtime: build with -tags=\"onnx,ORT\"") +} + +// ProcessImage returns an error for the stub. +func (g *T5Gemma2Generator) ProcessImage(imageData []byte) ([]float32, error) { + return nil, errors.New("T5Gemma-2 generator requires ONNX Runtime: build with -tags=\"onnx,ORT\"") +} + +// HasVision returns false for the stub. +func (g *T5Gemma2Generator) HasVision() bool { + return false +} + +// Config returns an empty configuration for the stub. +func (g *T5Gemma2Generator) Config() T5Gemma2GeneratorConfig { + return T5Gemma2GeneratorConfig{} +} + +// Close is a no-op for the stub. +func (g *T5Gemma2Generator) Close() error { + return nil +} + +// IsT5Gemma2GeneratorModel checks if a model directory contains T5Gemma-2 generator files. +func IsT5Gemma2GeneratorModel(modelPath string) bool { + // Must have standard seq2seq files + requiredFiles := []string{"encoder.onnx", "decoder.onnx", "decoder-init.onnx"} + for _, file := range requiredFiles { + filePath := filepath.Join(modelPath, file) + if _, err := os.Stat(filePath); err != nil { + return false + } + } + + // Check config for model_type: "t5gemma2" + configPath := filepath.Join(modelPath, "config.json") + if data, err := os.ReadFile(configPath); err == nil { + var config struct { + ModelType string `json:"model_type"` + } + if err := json.Unmarshal(data, &config); err == nil { + return config.ModelType == "t5gemma2" + } + } + + // Also check t5gemma2_config.json + t5gemma2ConfigPath := filepath.Join(modelPath, "t5gemma2_config.json") + if _, err := os.Stat(t5gemma2ConfigPath); err == nil { + return true + } + + return false +} + +// DecodeBase64Image decodes a base64-encoded image (data URI or raw base64). +func DecodeBase64Image(input string) ([]byte, error) { + if strings.HasPrefix(input, "data:") { + parts := strings.SplitN(input, ",", 2) + if len(parts) != 2 { + return nil, errors.New("invalid data URI format") + } + input = parts[1] + } + + return base64.StdEncoding.DecodeString(input) +} diff --git a/pkg/termite/model_registry.go b/pkg/termite/model_registry.go index 33b14f1..9d6d342 100644 --- a/pkg/termite/model_registry.go +++ b/pkg/termite/model_registry.go @@ -64,12 +64,56 @@ func isMultimodalModel(modelPath string) (hasStandard, hasQuantized bool) { return } +// isT5Gemma2Model checks if a model directory contains T5Gemma-2 model files. +// T5Gemma-2 models have encoder.onnx + vision_encoder.onnx. +func isT5Gemma2Model(modelPath string) bool { + encoderPath := filepath.Join(modelPath, "encoder.onnx") + visionPath := filepath.Join(modelPath, "vision_encoder.onnx") + + // Must have both encoder and vision encoder + if !fileExistsRegistry(encoderPath) || !fileExistsRegistry(visionPath) { + return false + } + + // Check config.json for model_type: "t5gemma2" + configPath := filepath.Join(modelPath, "config.json") + if data, err := os.ReadFile(configPath); err == nil { + var config struct { + ModelType string `json:"model_type"` + } + if err := json.Unmarshal(data, &config); err == nil { + return config.ModelType == "t5gemma2" + } + } + + // Also check t5gemma2_config.json + t5gemma2ConfigPath := filepath.Join(modelPath, "t5gemma2_config.json") + return fileExistsRegistry(t5gemma2ConfigPath) +} + // fileExistsRegistry checks if a file exists func fileExistsRegistry(path string) bool { _, err := os.Stat(path) return err == nil } +// isDirectoryEntry checks if an entry is a directory, following symlinks. +// This is needed because os.DirEntry.IsDir() returns false for symlinks. +func isDirectoryEntry(path string, entry os.DirEntry) bool { + // Fast path: regular directory + if entry.IsDir() { + return true + } + // Check if it's a symlink to a directory + if entry.Type()&os.ModeSymlink != 0 { + info, err := os.Stat(path) // Stat follows symlinks + if err == nil && info.IsDir() { + return true + } + } + return false +} + // DiscoveredModel represents a model found during directory scanning type DiscoveredModel struct { // Ref is the parsed model reference (owner/name) @@ -113,7 +157,8 @@ func discoverModelsInDir(modelsDir string, modelType modelregistry.ModelType, lo } for _, ownerEntry := range ownerEntries { - if !ownerEntry.IsDir() { + // Check if it's a directory (or symlink to directory) + if !isDirectoryEntry(filepath.Join(modelsDir, ownerEntry.Name()), ownerEntry) { continue } @@ -128,7 +173,8 @@ func discoverModelsInDir(modelsDir string, modelType modelregistry.ModelType, lo } for _, modelEntry := range modelEntries { - if !modelEntry.IsDir() { + // Check if it's a directory (or symlink to directory) + if !isDirectoryEntry(filepath.Join(ownerPath, modelEntry.Name()), modelEntry) { continue } modelPath := filepath.Join(ownerPath, modelEntry.Name()) diff --git a/pkg/termite/seq2seq_registry.go b/pkg/termite/seq2seq_registry.go index 11144d2..e115e87 100644 --- a/pkg/termite/seq2seq_registry.go +++ b/pkg/termite/seq2seq_registry.go @@ -26,6 +26,7 @@ import ( "github.com/antflydb/termite/pkg/termite/lib/seq2seq" "github.com/jellydator/ttlcache/v3" "go.uber.org/zap" + "golang.org/x/sync/singleflight" ) // Seq2SeqModelInfo holds metadata about a discovered Seq2Seq model (not loaded yet) @@ -47,6 +48,9 @@ type Seq2SeqRegistry struct { // Loaded models with TTL cache cache *ttlcache.Cache[string, seq2seq.Model] + // Singleflight to prevent concurrent loads of the same model + sfGroup singleflight.Group + // Configuration keepAlive time.Duration maxLoadedModels uint64 @@ -189,9 +193,10 @@ func (r *Seq2SeqRegistry) discoverModels() error { return nil } -// Get returns a Seq2Seq model by name, loading it if necessary +// Get returns a Seq2Seq model by name, loading it if necessary. +// Uses singleflight to prevent concurrent loads of the same model. func (r *Seq2SeqRegistry) Get(modelName string) (seq2seq.Model, error) { - // Check cache first + // Check cache first (fast path) if item := r.cache.Get(modelName); item != nil { r.logger.Debug("Seq2Seq cache hit", zap.String("model", modelName)) return item.Value(), nil @@ -206,8 +211,29 @@ func (r *Seq2SeqRegistry) Get(modelName string) (seq2seq.Model, error) { return nil, fmt.Errorf("Seq2Seq model not found: %s", modelName) } - // Load the model - return r.loadModel(info) + // Use singleflight to prevent concurrent loads of the same model + result, err, shared := r.sfGroup.Do(modelName, func() (interface{}, error) { + // Double-check cache in case another goroutine loaded while we waited + if item := r.cache.Get(modelName); item != nil { + r.logger.Debug("Seq2Seq cache hit during singleflight", + zap.String("model", modelName)) + return item.Value(), nil + } + + // Load the model + return r.loadModel(info) + }) + + if err != nil { + return nil, err + } + + if shared { + r.logger.Debug("Seq2Seq singleflight shared result", + zap.String("model", modelName)) + } + + return result.(seq2seq.Model), nil } // GetQuestionGenerator returns a Seq2Seq model as a QuestionGenerator by name @@ -230,19 +256,46 @@ func (r *Seq2SeqRegistry) loadModel(info *Seq2SeqModelInfo) (seq2seq.Model, erro zap.String("model", info.Name), zap.String("path", info.Path)) - // Load the Seq2Seq model - model, backendUsed, err := seq2seq.NewHugotSeq2SeqWithSessionManager( - info.Path, r.sessionManager, nil, r.logger.Named(info.Name)) - if err != nil { - return nil, fmt.Errorf("loading Seq2Seq model %s: %w", info.Name, err) - } + var model seq2seq.Model + var backendUsed string + + // Check if this is a T5Gemma-2 model (multimodal seq2seq) + if seq2seq.IsT5Gemma2GeneratorModel(info.Path) { + r.logger.Info("Detected T5Gemma-2 generator model", + zap.String("model", info.Name)) - config := model.Config() - r.logger.Info("Successfully loaded Seq2Seq model", - zap.String("name", info.Name), - zap.String("task", config.Task), - zap.Int("max_length", config.MaxLength), - zap.String("backend", string(backendUsed))) + t5Model, backend, err := seq2seq.NewT5Gemma2GeneratorWithSessionManager( + info.Path, r.sessionManager, nil, r.logger.Named(info.Name)) + if err != nil { + return nil, fmt.Errorf("loading T5Gemma-2 model %s: %w", info.Name, err) + } + model = t5Model + backendUsed = string(backend) + + config := t5Model.Config() + r.logger.Info("Successfully loaded T5Gemma-2 generator", + zap.String("name", info.Name), + zap.String("task", config.Task), + zap.Int("max_new_tokens", config.MaxNewTokens), + zap.Bool("has_vision", t5Model.HasVision()), + zap.String("backend", backendUsed)) + } else { + // Load as standard HugotSeq2Seq model + hugotModel, backend, err := seq2seq.NewHugotSeq2SeqWithSessionManager( + info.Path, r.sessionManager, nil, r.logger.Named(info.Name)) + if err != nil { + return nil, fmt.Errorf("loading Seq2Seq model %s: %w", info.Name, err) + } + model = hugotModel + backendUsed = string(backend) + + config := hugotModel.Config() + r.logger.Info("Successfully loaded Seq2Seq model", + zap.String("name", info.Name), + zap.String("task", config.Task), + zap.Int("max_length", config.MaxLength), + zap.String("backend", backendUsed)) + } // Add to cache r.cache.Set(info.Name, model, r.keepAlive) @@ -262,6 +315,13 @@ func (r *Seq2SeqRegistry) List() []string { return names } +// GetModelInfo returns the model info for a discovered model (not necessarily loaded). +func (r *Seq2SeqRegistry) GetModelInfo(modelName string) *Seq2SeqModelInfo { + r.mu.RLock() + defer r.mu.RUnlock() + return r.discovered[modelName] +} + // ListLoaded returns only the currently loaded Seq2Seq model names func (r *Seq2SeqRegistry) ListLoaded() []string { return r.cache.Keys() diff --git a/scripts/export_model_to_registry.py b/scripts/export_model_to_registry.py index bb05881..6c76da0 100755 --- a/scripts/export_model_to_registry.py +++ b/scripts/export_model_to_registry.py @@ -108,7 +108,7 @@ ) logger = logging.getLogger(__name__) -ModelType = Literal["embedder", "reranker", "chunker", "recognizer", "rewriter", "generator"] +ModelType = Literal["embedder", "reranker", "chunker", "recognizer", "rewriter", "generator", "t5gemma2"] # Recognizer capabilities - these describe what extraction tasks the model supports # Used in manifest to advertise model capabilities to Termite @@ -151,6 +151,11 @@ "default_model": "google/gemma-3-1b-it", "dir_name": "generators", }, + "t5gemma2": { + "ort_class": None, # Uses custom export script + "default_model": "google/t5gemma-2-270m-270m", + "dir_name": "t5gemma2", + }, } # Files to include in the manifest (in order of importance) @@ -237,6 +242,24 @@ "generation_config.json", ] +# Files for T5Gemma-2 multimodal encoder-decoder models +T5GEMMA2_MANIFEST_FILES = [ + "encoder.onnx", + "encoder.onnx_data", + "vision_encoder.onnx", + "vision_encoder.onnx_data", + "decoder-init.onnx", + "decoder-init.onnx_data", + "decoder.onnx", + "decoder.onnx_data", + "tokenizer.json", + "config.json", + "tokenizer_config.json", + "special_tokens_map.json", + "t5gemma2_config.json", + "preprocessor_config.json", +] + def detect_recognizer_type(model_id: str) -> tuple[str, list[str]]: """ @@ -1087,6 +1110,78 @@ def detect_and_add_tool_call_format(model_dir: Path) -> None: logger.warning(f"Failed to update genai_config.json: {e}") +def export_t5gemma2_model( + model_id: str, + output_dir: Path, + variants: list[str] | None = None, +) -> Path: + """ + Export a T5Gemma-2 multimodal encoder-decoder model to ONNX format. + + Uses the custom export_t5gemma2.py script which handles: + - Text encoder export (encoder.onnx) + - Vision encoder export (vision_encoder.onnx) if multimodal + - Decoder export (decoder-init.onnx, decoder.onnx) + - Tokenizer and configuration files + + Args: + model_id: HuggingFace model ID (e.g., google/t5gemma-2-270m-270m) + output_dir: Directory to save the model + variants: List of variant types (not used currently, reserved for future) + """ + import subprocess + + output_dir.mkdir(parents=True, exist_ok=True) + + logger.info(f"Exporting T5Gemma-2 model: {model_id}") + logger.info(f"Output: {output_dir}") + + # Get the path to the export script + script_dir = Path(__file__).parent + export_script = script_dir / "export_t5gemma2.py" + + if not export_script.exists(): + raise RuntimeError(f"T5Gemma-2 export script not found: {export_script}") + + # Build the export command + cmd = [ + "python", str(export_script), + "--model", model_id, + "--output", str(output_dir), + ] + + logger.info(f"Running: {' '.join(cmd)}") + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + if result.stdout: + for line in result.stdout.strip().split('\n'): + logger.info(f" {line}") + logger.info("T5Gemma-2 export completed successfully") + except subprocess.CalledProcessError as e: + logger.error("T5Gemma-2 export failed") + if e.stdout: + logger.error(f"stdout: {e.stdout}") + if e.stderr: + logger.error(f"stderr: {e.stderr}") + raise RuntimeError(f"T5Gemma-2 export failed: {e.stderr}") + + # Log exported files + logger.info("\nExported files:") + for f in sorted(output_dir.rglob("*")): + if f.is_file(): + size_mb = f.stat().st_size / (1024 * 1024) + rel_path = f.relative_to(output_dir) + logger.info(f" {rel_path}: {size_mb:.2f} MB") + + return output_dir + + def generate_manifest( model_type: ModelType, model_name: str, @@ -1117,10 +1212,13 @@ def generate_manifest( # Use appropriate file list based on model type and capabilities is_multimodal = capabilities and "multimodal" in capabilities is_generator = model_type == "generator" + is_t5gemma2 = model_type == "t5gemma2" is_gliner = recognizer_arch == "gliner" is_rebel = recognizer_arch == "rebel" - if is_multimodal: + if is_t5gemma2: + file_list = T5GEMMA2_MANIFEST_FILES + elif is_multimodal: file_list = MULTIMODAL_MANIFEST_FILES elif model_type == "rewriter": file_list = SEQ2SEQ_MANIFEST_FILES @@ -1836,6 +1934,9 @@ def test_model( logger.info("Testing exported model...") capabilities = capabilities or [] + if model_type == "t5gemma2": + return test_t5gemma2_model(model_dir) + if "multimodal" in capabilities: return test_multimodal_model(model_dir) @@ -2251,6 +2352,103 @@ def test_generator_model(model_dir: Path) -> bool: return False +def test_t5gemma2_model(model_dir: Path) -> bool: + """Test a T5Gemma-2 multimodal encoder-decoder model.""" + import onnxruntime as ort + import numpy as np + + try: + # Check for required files + encoder_path = model_dir / "encoder.onnx" + decoder_init_path = model_dir / "decoder-init.onnx" + + if not encoder_path.exists(): + logger.error(f"encoder.onnx not found in {model_dir}") + return False + if not decoder_init_path.exists(): + logger.error(f"decoder-init.onnx not found in {model_dir}") + return False + + # Check for config + config_path = model_dir / "t5gemma2_config.json" + if config_path.exists(): + logger.info("Loading t5gemma2_config.json...") + with open(config_path) as f: + config = json.load(f) + logger.info(f" Model ID: {config.get('model_id', 'unknown')}") + logger.info(f" Hidden size: {config.get('hidden_size', 'unknown')}") + logger.info(f" Capabilities: {', '.join(config.get('capabilities', []))}") + + # Load encoder + logger.info("Loading encoder ONNX model...") + encoder_session = ort.InferenceSession( + str(encoder_path), + providers=["CPUExecutionProvider"], + ) + + # Get input info + encoder_inputs = encoder_session.get_inputs() + logger.info(f" Encoder inputs: {[i.name for i in encoder_inputs]}") + + # Create dummy inputs + batch_size = 1 + seq_len = 32 + + dummy_input_ids = np.ones((batch_size, seq_len), dtype=np.int64) + dummy_attention_mask = np.ones((batch_size, seq_len), dtype=np.int64) + + # Run encoder + logger.info("Running encoder inference...") + encoder_outputs = encoder_session.run( + None, + { + "input_ids": dummy_input_ids, + "attention_mask": dummy_attention_mask, + }, + ) + encoder_hidden_states = encoder_outputs[0] + logger.info(f" Encoder output shape: {encoder_hidden_states.shape}") + + # Test decoder-init + logger.info("Loading decoder-init ONNX model...") + decoder_init_session = ort.InferenceSession( + str(decoder_init_path), + providers=["CPUExecutionProvider"], + ) + + decoder_inputs = decoder_init_session.get_inputs() + logger.info(f" Decoder inputs: {[i.name for i in decoder_inputs]}") + + # Create dummy decoder inputs + dummy_decoder_input_ids = np.zeros((batch_size, 1), dtype=np.int64) + + logger.info("Running decoder-init inference...") + decoder_outputs = decoder_init_session.run( + None, + { + "input_ids": dummy_decoder_input_ids, + "encoder_hidden_states": encoder_hidden_states, + "encoder_attention_mask": dummy_attention_mask, + }, + ) + logits = decoder_outputs[0] + logger.info(f" Decoder logits shape: {logits.shape}") + + # Check vision encoder if present + vision_path = model_dir / "vision_encoder.onnx" + if vision_path.exists(): + logger.info("Vision encoder found (multimodal model)") + + logger.info("Test passed!") + return True + + except Exception as e: + logger.error(f"Test failed: {e}") + import traceback + traceback.print_exc() + return False + + def cmd_gc(args): """Handle the gc subcommand.""" endpoint = args.r2_endpoint or os.environ.get("AWS_ENDPOINT_URL") @@ -2314,7 +2512,12 @@ def cmd_export(args): # Export model using appropriate function logger.info("\n[1/4] Exporting model to ONNX...") hf_token = getattr(args, "hf_token", None) - if args.model_type == "rewriter": + if args.model_type == "t5gemma2": + # Add default capabilities for T5Gemma-2 + if not capabilities: + capabilities = ["embeddings", "generation", "decoding", "multimodal"] + export_t5gemma2_model(model_id, model_dir, args.variants) + elif args.model_type == "rewriter": export_seq2seq_model(model_id, model_dir, args.variants) elif args.model_type == "generator": export_generator_model(model_id, model_dir, args.variants, hf_token=hf_token) @@ -2494,7 +2697,7 @@ def main(): ) # Export subcommands (one for each model type) - for model_type in ["embedder", "reranker", "chunker", "recognizer", "rewriter", "generator"]: + for model_type in ["embedder", "reranker", "chunker", "recognizer", "rewriter", "generator", "t5gemma2"]: export_parser = subparsers.add_parser( model_type, help=f"Export a {model_type} model to ONNX", diff --git a/scripts/export_t5gemma2.py b/scripts/export_t5gemma2.py new file mode 100755 index 0000000..f6371e6 --- /dev/null +++ b/scripts/export_t5gemma2.py @@ -0,0 +1,1073 @@ +#!/usr/bin/env -S uv run +# /// script +# requires-python = ">=3.10" +# dependencies = [ +# "transformers @ git+https://github.com/huggingface/transformers.git", +# "torch>=2.6.0", +# "onnx>=1.15.0", +# "onnxruntime>=1.17.0", +# "onnxscript", +# "pillow", +# "requests", +# "numpy", +# "accelerate", +# "sentencepiece", +# ] +# /// +""" +Export T5Gemma-2 models to ONNX format for Termite. + +T5Gemma-2 is Google's encoder-decoder model adapted from Gemma 3 via UL2 training. +It has unique architectural features: + - Merged self/cross-attention in decoder + - Tied word embeddings between encoder and decoder + - Grouped Query Attention (GQA) + - Rotary Position Embeddings (RoPE) + - Alternating sliding window attention (4096 tokens) + - SigLIP vision encoder for multimodal + +This script exports 4 ONNX files: + 1. encoder.onnx - Text encoder (for embeddings and seq2seq) + 2. vision_encoder.onnx - SigLIP vision encoder (for image embeddings) + 3. decoder_init.onnx - Decoder without past_key_values (first token) + 4. decoder.onnx - Decoder with past_key_values (efficient generation) + +Usage: + # Export 270M variant (recommended for development) + ./scripts/export_t5gemma2.py --model google/t5gemma-2-270m-270m --output ./models/t5gemma2/270m + + # Export with testing + ./scripts/export_t5gemma2.py --model google/t5gemma-2-270m-270m --output ./models/t5gemma2/270m --test + + # Export 1B variant + ./scripts/export_t5gemma2.py --model google/t5gemma-2-1b-1b --output ./models/t5gemma2/1b + +Available Models: + - google/t5gemma-2-270m-270m (~800M total params, fast) + - google/t5gemma-2-1b-1b (~2B total params, balanced) + - google/t5gemma-2-4b-4b (~8B total params, best quality) + +References: + - HuggingFace: https://huggingface.co/docs/transformers/model_doc/t5gemma2 + - Google Blog: https://blog.google/technology/developers/t5gemma-2/ + - Paper: https://arxiv.org/abs/2512.14856 + +Prerequisites: + - uv (Python package manager): brew install uv + - HuggingFace account with access to T5Gemma-2 models + +Note: This script is self-contained and does NOT require optimum-onnx. +It applies the same ONNX compatibility patches manually. + +If you prefer using optimum-cli for export, use our fork: + https://github.com/timkaye11/optimum-onnx (branch: add-t5gemma2-support) + +See docs/T5GEMMA2.md for full setup instructions. +""" + +import argparse +import json +import logging +import shutil +import sys +from pathlib import Path +from typing import Optional + +import numpy as np +import os + +# Disable torch dynamo verbose mode +os.environ["TORCHDYNAMO_VERBOSE"] = "0" + +logging.basicConfig(level=logging.INFO, format="%(message)s") +logger = logging.getLogger(__name__) + +# ONNX opset version - use 17 for better compatibility with complex attention patterns +OPSET_VERSION = 17 + + +def disable_dynamo(): + """Disable torch dynamo to avoid tracing issues.""" + try: + import torch._dynamo + torch._dynamo.config.suppress_errors = True + torch._dynamo.reset() + # Completely disable dynamo + torch._dynamo.disable() + except Exception: + pass + + +def patch_transformers_for_onnx(): + """ + Patch transformers masking utilities for ONNX export compatibility. + + This applies the same patches as optimum-onnx's ModelPatcher, replacing + vmap-based mask creation with broadcasting-based alternatives that are + compatible with TorchScript/ONNX tracing. + + Based on: https://github.com/huggingface/optimum-onnx/blob/main/optimum/exporters/onnx/model_patcher.py + """ + try: + import torch + from typing import Callable, Optional + + import transformers.masking_utils as masking_utils + from transformers.masking_utils import ( + ALL_MASK_ATTENTION_FUNCTIONS, + and_masks, + causal_mask_function, + padding_mask_function, + ) + + def sdpa_mask_without_vmap( + batch_size: int, + cache_position: torch.Tensor, + kv_length: int, + kv_offset: int = 0, + mask_function: Optional[Callable] = None, + attention_mask: Optional[torch.Tensor] = None, + local_size: Optional[int] = None, + allow_is_causal_skip: bool = True, + **kwargs, + ) -> Optional[torch.Tensor]: + """ + Custom vectorized implementation of sdpa_mask without using vmap. + Uses broadcasting-based index creation instead. + """ + if mask_function is None: + mask_function = causal_mask_function + + q_length = cache_position.shape[0] + + # Potentially pad the 2D mask - handle the padding_length check safely + padding_mask = None + if attention_mask is not None: + padding_needed = kv_length + kv_offset - attention_mask.shape[-1] + if padding_needed > 0: + padding_mask = torch.nn.functional.pad(attention_mask, (0, padding_needed)) + else: + padding_mask = attention_mask + + # Potentially add the padding 2D mask + if padding_mask is not None: + mask_function = and_masks(mask_function, padding_mask_function(padding_mask)) + + # Create broadcatable indices (optimum-onnx approach) + device = cache_position.device + q_indices = cache_position[None, None, :, None] + head_indices = torch.arange(1, dtype=torch.long, device=device)[None, :, None, None] + batch_indices = torch.arange(batch_size, dtype=torch.long, device=device)[:, None, None, None] + kv_indices = torch.arange(kv_length, dtype=torch.long, device=device)[None, None, None, :] + kv_offset + + # Apply mask function element-wise through broadcasting + causal_mask = mask_function(batch_indices, head_indices, q_indices, kv_indices) + + # Expand the mask to match batch size and query length + causal_mask = causal_mask.expand(batch_size, -1, q_length, kv_length) + + return causal_mask + + def eager_mask_without_vmap( + batch_size: int, + cache_position: torch.Tensor, + kv_length: int, + kv_offset: int = 0, + mask_function: Optional[Callable] = None, + attention_mask: Optional[torch.Tensor] = None, + dtype: torch.dtype = torch.float32, + **kwargs, + ) -> torch.Tensor: + """ + Eager attention mask without vmap, adapted from transformers. + Converts boolean mask to float mask with 0 for attend, -inf for mask. + """ + kwargs.pop("allow_is_causal_skip", None) + kwargs.pop("allow_is_bidirectional_skip", None) + kwargs.pop("allow_torch_fix", None) + + mask = sdpa_mask_without_vmap( + batch_size=batch_size, + cache_position=cache_position, + kv_length=kv_length, + kv_offset=kv_offset, + mask_function=mask_function, + attention_mask=attention_mask, + allow_is_causal_skip=False, + **kwargs, + ) + + if mask is not None: + min_dtype = torch.finfo(dtype).min + # Convert bool mask to float: True -> 0.0, False -> -inf + mask = torch.where(mask, torch.zeros((), device=mask.device, dtype=dtype), min_dtype) + + return mask + + # Register the patched functions + ALL_MASK_ATTENTION_FUNCTIONS.register("sdpa", sdpa_mask_without_vmap) + ALL_MASK_ATTENTION_FUNCTIONS.register("eager", eager_mask_without_vmap) + + # Patch find_packed_sequence_indices to avoid torch.diff (not supported in ONNX) + def find_packed_sequence_indices_onnx(position_ids: torch.Tensor) -> Optional[torch.Tensor]: + """ + ONNX-compatible version that always returns None (no packed sequences). + This is safe for export since we're exporting with fixed batch size. + """ + return None + + masking_utils.find_packed_sequence_indices = find_packed_sequence_indices_onnx + + logger.info("Patched transformers masking utilities for ONNX compatibility (optimum-onnx style)") + return True + + except Exception as e: + logger.warning(f"Could not patch transformers masking utilities: {e}") + import traceback + traceback.print_exc() + return False + + +def check_dependencies(): + """Check if required packages are installed.""" + missing = [] + try: + import torch + except ImportError: + missing.append("torch>=2.0.0") + try: + import transformers + except ImportError: + missing.append("transformers>=4.48.0") + try: + import onnx + except ImportError: + missing.append("onnx>=1.15.0") + try: + import onnxruntime + except ImportError: + missing.append("onnxruntime>=1.17.0") + + if missing: + print(f"Missing required packages: {', '.join(missing)}") + print(f"Install with: pip install {' '.join(missing)}") + sys.exit(1) + + +class T5Gemma2EncoderWrapper: + """Wrapper to export just the encoder portion of T5Gemma2.""" + + def __init__(self, model): + self.encoder = model.encoder + self.embed_tokens = model.encoder.embed_tokens + + def forward(self, input_ids, attention_mask): + # Get embeddings + inputs_embeds = self.embed_tokens(input_ids) + + # Run encoder + encoder_outputs = self.encoder( + inputs_embeds=inputs_embeds, + attention_mask=attention_mask, + output_hidden_states=False, + return_dict=True, + ) + + return encoder_outputs.last_hidden_state + + +class T5Gemma2DecoderInitWrapper: + """Wrapper to export decoder without past_key_values (first token generation).""" + + def __init__(self, model): + self.decoder = model.decoder + self.lm_head = model.lm_head + self.embed_tokens = model.decoder.embed_tokens + + def forward(self, decoder_input_ids, encoder_hidden_states, encoder_attention_mask): + # Get decoder embeddings + inputs_embeds = self.embed_tokens(decoder_input_ids) + + # Run decoder without past + decoder_outputs = self.decoder( + inputs_embeds=inputs_embeds, + encoder_hidden_states=encoder_hidden_states, + encoder_attention_mask=encoder_attention_mask, + use_cache=True, + return_dict=True, + ) + + # Get logits + logits = self.lm_head(decoder_outputs.last_hidden_state) + + # Return logits and past_key_values for subsequent decoding + return logits, decoder_outputs.past_key_values + + +class T5Gemma2DecoderWrapper: + """Wrapper to export decoder with past_key_values (efficient generation).""" + + def __init__(self, model): + self.decoder = model.decoder + self.lm_head = model.lm_head + self.embed_tokens = model.decoder.embed_tokens + + def forward( + self, + decoder_input_ids, + encoder_hidden_states, + encoder_attention_mask, + past_key_values, + ): + # Get decoder embeddings + inputs_embeds = self.embed_tokens(decoder_input_ids) + + # Run decoder with past + decoder_outputs = self.decoder( + inputs_embeds=inputs_embeds, + encoder_hidden_states=encoder_hidden_states, + encoder_attention_mask=encoder_attention_mask, + past_key_values=past_key_values, + use_cache=True, + return_dict=True, + ) + + # Get logits + logits = self.lm_head(decoder_outputs.last_hidden_state) + + return logits, decoder_outputs.past_key_values + + +def export_encoder(model, processor, output_dir: Path) -> None: + """Export the text encoder to ONNX. + + Due to T5Gemma-2's complex attention implementation, we export two options: + 1. embedding_layer.onnx - Just the embedding lookup (fast, always works) + 2. encoder.onnx - Full encoder with attention (may fail with complex models) + """ + import torch + import onnx + + logger.info("\n1. Exporting text encoder...") + + # Create dummy inputs + dummy_text = "This is a test sentence for the encoder." + inputs = processor.tokenizer( + dummy_text, + return_tensors="pt", + padding="max_length", + max_length=128, + truncation=True, + ) + + input_ids = inputs["input_ids"] + attention_mask = inputs["attention_mask"] + + # Get encoder from model using get_encoder() method + encoder = model.get_encoder() + + # Get embed_tokens from the model's text_embed_tokens or from encoder + if hasattr(model.model, 'text_embed_tokens'): + embed_tokens = model.model.text_embed_tokens + elif hasattr(encoder, 'embed_tokens'): + embed_tokens = encoder.embed_tokens + else: + # Fallback: try to get from decoder's embed_tokens (they may be shared) + decoder = model.get_decoder() + embed_tokens = decoder.embed_tokens + + # Export 1: Embedding layer only (simpler, always works) + embedding_path = output_dir / "embedding_layer.onnx" + logger.info(" 1a. Exporting embedding layer...") + + class EmbeddingModule(torch.nn.Module): + def __init__(self, embed_tokens): + super().__init__() + self.embed_tokens = embed_tokens + + def forward(self, input_ids): + return self.embed_tokens(input_ids) + + embedding_module = EmbeddingModule(embed_tokens) + embedding_module.eval() + for param in embedding_module.parameters(): + param.requires_grad = False + + with torch.no_grad(): + torch.onnx.export( + embedding_module, + (input_ids,), + str(embedding_path), + export_params=True, + opset_version=OPSET_VERSION, + do_constant_folding=True, + input_names=["input_ids"], + output_names=["embeddings"], + dynamic_axes={ + "input_ids": {0: "batch_size", 1: "sequence_length"}, + "embeddings": {0: "batch_size", 1: "sequence_length"}, + }, + dynamo=False, + ) + + # Validate embedding layer + onnx_model = onnx.load(str(embedding_path)) + onnx.checker.check_model(onnx_model) + size_mb = embedding_path.stat().st_size / (1024 * 1024) + logger.info(f" Saved: embedding_layer.onnx ({size_mb:.1f} MB)") + + # Export 2: Full encoder (may fail due to complex attention) + encoder_path = output_dir / "encoder.onnx" + logger.info(" 1b. Exporting full encoder (this may take a while)...") + + class EncoderModule(torch.nn.Module): + def __init__(self, encoder, embed_tokens): + super().__init__() + self.encoder = encoder + self.embed_tokens = embed_tokens + + def forward(self, input_ids, attention_mask): + inputs_embeds = self.embed_tokens(input_ids) + outputs = self.encoder( + inputs_embeds=inputs_embeds, + attention_mask=attention_mask, + output_hidden_states=False, + return_dict=True, + ) + return outputs.last_hidden_state + + encoder_module = EncoderModule(encoder, embed_tokens) + encoder_module.eval() + for param in encoder_module.parameters(): + param.requires_grad = False + + try: + with torch.no_grad(): + torch.onnx.export( + encoder_module, + (input_ids, attention_mask), + str(encoder_path), + export_params=True, + opset_version=OPSET_VERSION, + do_constant_folding=True, + input_names=["input_ids", "attention_mask"], + output_names=["encoder_hidden_states"], + dynamic_axes={ + "input_ids": {0: "batch_size", 1: "sequence_length"}, + "attention_mask": {0: "batch_size", 1: "sequence_length"}, + "encoder_hidden_states": {0: "batch_size", 1: "sequence_length"}, + }, + dynamo=False, + ) + + # Validate + onnx_model = onnx.load(str(encoder_path)) + onnx.checker.check_model(onnx_model) + size_mb = encoder_path.stat().st_size / (1024 * 1024) + logger.info(f" Saved: encoder.onnx ({size_mb:.1f} MB)") + + # Test encoder output shape + with torch.no_grad(): + test_output = encoder_module(input_ids, attention_mask) + logger.info(f" Encoder output shape: {test_output.shape}") + + except Exception as e: + logger.warning(f" Full encoder export failed: {e}") + logger.info(" Using embedding_layer.onnx for embeddings (without attention)") + logger.info(" For full encoder functionality, use PyTorch inference instead of ONNX") + + +def export_vision_encoder(model, processor, output_dir: Path) -> bool: + """Export the vision encoder (SigLIP) to ONNX if present.""" + import torch + import onnx + + # Check if model has vision encoder - try multiple paths + vision_tower = None + encoder = model.get_encoder() + + # Try different possible locations for vision tower + if hasattr(encoder, 'vision_tower'): + vision_tower = encoder.vision_tower + elif hasattr(model.model, 'vision_tower'): + vision_tower = model.model.vision_tower + elif hasattr(model, 'vision_tower'): + vision_tower = model.vision_tower + + if vision_tower is None: + logger.info("\n2. No vision encoder found (text-only model)") + return False + + logger.info("\n2. Exporting vision encoder (SigLIP)...") + + vision_path = output_dir / "vision_encoder.onnx" + + # Get image size from processor + if hasattr(processor, "image_processor"): + image_size = processor.image_processor.size.get("height", 896) + else: + image_size = 896 # Default for T5Gemma2 + + # Create dummy image input + dummy_pixel_values = torch.randn(1, 3, image_size, image_size) + + class VisionEncoderModule(torch.nn.Module): + def __init__(self, vision_tower): + super().__init__() + self.vision_tower = vision_tower + + def forward(self, pixel_values): + outputs = self.vision_tower(pixel_values, return_dict=True) + return outputs.last_hidden_state + + vision_module = VisionEncoderModule(vision_tower) + vision_module.eval() + + try: + with torch.no_grad(): + torch.onnx.export( + vision_module, + (dummy_pixel_values,), + str(vision_path), + export_params=True, + opset_version=OPSET_VERSION, + do_constant_folding=True, + input_names=["pixel_values"], + output_names=["vision_hidden_states"], + dynamic_axes={ + "pixel_values": {0: "batch_size"}, + "vision_hidden_states": {0: "batch_size"}, + }, + dynamo=False, # Use legacy exporter + ) + + # Validate + onnx_model = onnx.load(str(vision_path)) + onnx.checker.check_model(onnx_model) + + size_mb = vision_path.stat().st_size / (1024 * 1024) + logger.info(f" Saved: vision_encoder.onnx ({size_mb:.1f} MB)") + return True + + except Exception as e: + logger.warning(f" Vision encoder export failed: {e}") + return False + + +def export_decoder_init(model, processor, output_dir: Path, hidden_size: int) -> None: + """Export the decoder without past_key_values (for first token generation).""" + import torch + import onnx + + logger.info("\n3. Exporting decoder-init (without past_key_values)...") + + decoder_init_path = output_dir / "decoder-init.onnx" + + # Get decoder components using get_decoder() method + decoder = model.get_decoder() + lm_head = model.lm_head + + # Get embed_tokens - try multiple paths + if hasattr(decoder, 'embed_tokens'): + embed_tokens = decoder.embed_tokens + elif hasattr(model.model, 'text_embed_tokens'): + embed_tokens = model.model.text_embed_tokens + elif hasattr(model.model, 'embed_tokens'): + embed_tokens = model.model.embed_tokens + else: + raise AttributeError("Could not find embed_tokens in model") + + # Create dummy inputs + batch_size = 1 + encoder_seq_len = 32 + decoder_seq_len = 1 + + dummy_decoder_input_ids = torch.zeros((batch_size, decoder_seq_len), dtype=torch.long) + dummy_encoder_hidden_states = torch.randn(batch_size, encoder_seq_len, hidden_size) + dummy_encoder_attention_mask = torch.ones((batch_size, encoder_seq_len), dtype=torch.long) + + class DecoderInitModule(torch.nn.Module): + def __init__(self, decoder, lm_head, embed_tokens): + super().__init__() + self.decoder = decoder + self.lm_head = lm_head + self.embed_tokens = embed_tokens + + def forward(self, decoder_input_ids, encoder_hidden_states, encoder_attention_mask): + inputs_embeds = self.embed_tokens(decoder_input_ids) + outputs = self.decoder( + inputs_embeds=inputs_embeds, + encoder_hidden_states=encoder_hidden_states, + encoder_attention_mask=encoder_attention_mask, + use_cache=False, # No cache for init + return_dict=True, + ) + logits = self.lm_head(outputs.last_hidden_state) + return logits + + decoder_init_module = DecoderInitModule(decoder, lm_head, embed_tokens) + decoder_init_module.eval() + + with torch.no_grad(): + torch.onnx.export( + decoder_init_module, + (dummy_decoder_input_ids, dummy_encoder_hidden_states, dummy_encoder_attention_mask), + str(decoder_init_path), + export_params=True, + opset_version=OPSET_VERSION, + do_constant_folding=True, + input_names=["input_ids", "encoder_hidden_states", "encoder_attention_mask"], + output_names=["logits"], + dynamic_axes={ + "input_ids": {0: "batch_size", 1: "decoder_sequence_length"}, + "encoder_hidden_states": {0: "batch_size", 1: "encoder_sequence_length"}, + "encoder_attention_mask": {0: "batch_size", 1: "encoder_sequence_length"}, + "logits": {0: "batch_size", 1: "decoder_sequence_length"}, + }, + dynamo=False, # Use legacy exporter + ) + + # Validate + onnx_model = onnx.load(str(decoder_init_path)) + onnx.checker.check_model(onnx_model) + + size_mb = decoder_init_path.stat().st_size / (1024 * 1024) + logger.info(f" Saved: decoder-init.onnx ({size_mb:.1f} MB)") + + +def export_decoder_with_past(model, processor, output_dir: Path, hidden_size: int) -> None: + """ + Export the decoder with past_key_values for efficient autoregressive generation. + + Note: This is complex due to T5Gemma2's merged self/cross-attention and GQA. + For initial implementation, we use decoder-init for all steps (slower but simpler). + """ + import torch + + logger.info("\n4. Exporting decoder with past_key_values...") + logger.info(" Note: Full KV cache export is complex due to merged attention.") + logger.info(" Using decoder-init for all steps (slower but functional).") + + # For now, we'll copy decoder-init as decoder (same file, different name) + # This means generation will be slower but will work correctly + decoder_init_path = output_dir / "decoder-init.onnx" + decoder_path = output_dir / "decoder.onnx" + + if decoder_init_path.exists(): + shutil.copy(str(decoder_init_path), str(decoder_path)) + size_mb = decoder_path.stat().st_size / (1024 * 1024) + logger.info(f" Saved: decoder.onnx ({size_mb:.1f} MB)") + logger.info(" (Using decoder-init format - no KV cache optimization)") + + +def save_configs(model, processor, output_dir: Path, model_id: str) -> None: + """Save model and tokenizer configuration files.""" + logger.info("\n5. Saving configuration files...") + + # Save tokenizer + processor.tokenizer.save_pretrained(str(output_dir)) + logger.info(" Saved: tokenizer files") + + # Save model config + model.config.save_pretrained(str(output_dir)) + logger.info(" Saved: config.json") + + # Save image processor config if available + if hasattr(processor, 'image_processor') and processor.image_processor is not None: + try: + processor.image_processor.save_pretrained(str(output_dir)) + logger.info(" Saved: preprocessor_config.json") + except Exception as e: + logger.warning(f" Could not save image processor config: {e}") + + # Extract config values with fallbacks for different config structures + def get_config_value(config, *paths, default=None): + """Try multiple paths to get a config value.""" + for path in paths: + obj = config + try: + for key in path.split('.'): + obj = getattr(obj, key) + return obj + except AttributeError: + continue + return default + + hidden_size = get_config_value( + model.config, + 'decoder.hidden_size', + 'hidden_size', + default=2304 + ) + vocab_size = get_config_value( + model.config, + 'decoder.vocab_size', + 'vocab_size', + default=262208 + ) + num_encoder_layers = get_config_value( + model.config, + 'encoder.text_config.num_hidden_layers', + 'encoder.num_hidden_layers', + 'num_hidden_layers', + default=26 + ) + num_decoder_layers = get_config_value( + model.config, + 'decoder.num_hidden_layers', + 'num_hidden_layers', + default=26 + ) + num_attention_heads = get_config_value( + model.config, + 'decoder.num_attention_heads', + 'num_attention_heads', + default=8 + ) + num_key_value_heads = get_config_value( + model.config, + 'decoder.num_key_value_heads', + 'num_key_value_heads', + default=4 + ) + + # Create T5Gemma2-specific config for Termite + t5gemma2_config = { + "model_id": model_id, + "model_type": "t5gemma2", + "task": "multimodal_seq2seq", + "capabilities": ["embeddings", "generation", "decoding", "multimodal"], + "max_encoder_length": 131072, # 128K + "max_decoder_length": 32768, # 32K output + "hidden_size": hidden_size, + "vocab_size": vocab_size, + "num_encoder_layers": num_encoder_layers, + "num_decoder_layers": num_decoder_layers, + "num_attention_heads": num_attention_heads, + "num_key_value_heads": num_key_value_heads, + "generation_config": { + "max_new_tokens": 256, + "num_beams": 1, + "do_sample": False, + "temperature": 1.0, + "top_p": 1.0, + }, + } + + config_path = output_dir / "t5gemma2_config.json" + with open(config_path, "w") as f: + json.dump(t5gemma2_config, f, indent=2) + logger.info(" Saved: t5gemma2_config.json") + + +def export_model(model_id: str, output_dir: str) -> None: + """ + Export a T5Gemma-2 model to ONNX format. + + Args: + model_id: HuggingFace model ID (e.g., 'google/t5gemma-2-270m-270m') + output_dir: Directory to save the exported model + """ + import torch + from transformers import AutoTokenizer, AutoModelForSeq2SeqLM + + # Disable dynamo and patch transformers before any model operations + disable_dynamo() + patch_transformers_for_onnx() + + output_path = Path(output_dir) + output_path.mkdir(parents=True, exist_ok=True) + + logger.info(f"Exporting T5Gemma-2: {model_id}") + logger.info(f"Output: {output_dir}") + + # Load model and tokenizer + logger.info("\nLoading model and tokenizer...") + logger.info("(This may take a while for larger models)") + + # Load tokenizer directly (AutoProcessor has compatibility issues with T5Gemma-2) + tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True) + + # Try to load image processor separately if available (for multimodal) + image_processor = None + try: + from transformers import AutoImageProcessor + image_processor = AutoImageProcessor.from_pretrained(model_id, trust_remote_code=True) + logger.info("Loaded image processor for multimodal support") + except Exception as e: + logger.info(f"No image processor found (text-only mode): {e}") + + # Create a simple processor-like object that has tokenizer attribute + class SimpleProcessor: + def __init__(self, tokenizer, image_processor=None): + self.tokenizer = tokenizer + self.image_processor = image_processor + + processor = SimpleProcessor(tokenizer, image_processor) + + # Load model with configuration optimized for ONNX export + from transformers import AutoConfig + config = AutoConfig.from_pretrained(model_id, trust_remote_code=True) + + # Set sliding window to a very large value to effectively disable it + # (Setting to None breaks other code that expects an int) + VERY_LARGE_WINDOW = 2**20 # 1M tokens + if hasattr(config, 'encoder'): + if hasattr(config.encoder, 'sliding_window'): + config.encoder.sliding_window = VERY_LARGE_WINDOW + if hasattr(config.encoder, 'sliding_window_size'): + config.encoder.sliding_window_size = VERY_LARGE_WINDOW + if hasattr(config, 'decoder'): + if hasattr(config.decoder, 'sliding_window'): + config.decoder.sliding_window = VERY_LARGE_WINDOW + if hasattr(config.decoder, 'sliding_window_size'): + config.decoder.sliding_window_size = VERY_LARGE_WINDOW + + model = AutoModelForSeq2SeqLM.from_pretrained( + model_id, + config=config, + torch_dtype=torch.float32, # Use FP32 for ONNX export + device_map="cpu", + trust_remote_code=True, + attn_implementation="eager", # Use eager attention for ONNX compatibility + ) + model.eval() + + # Disable gradient checkpointing if enabled + if hasattr(model, 'gradient_checkpointing_disable'): + model.gradient_checkpointing_disable() + + # Patch model config to use large sliding window at runtime + VERY_LARGE_WINDOW = 2**20 # 1M tokens + for module in model.modules(): + if hasattr(module, 'sliding_window') and module.sliding_window is not None: + module.sliding_window = VERY_LARGE_WINDOW + if hasattr(module, 'config') and hasattr(module.config, 'sliding_window'): + if module.config.sliding_window is not None: + module.config.sliding_window = VERY_LARGE_WINDOW + + # Get hidden size from config with fallbacks + def get_config_value(config, *paths, default=None): + """Try multiple paths to get a config value.""" + for path in paths: + obj = config + try: + for key in path.split('.'): + obj = getattr(obj, key) + return obj + except AttributeError: + continue + return default + + hidden_size = get_config_value( + model.config, + 'decoder.hidden_size', + 'hidden_size', + default=2304 + ) + logger.info(f"Model hidden size: {hidden_size}") + + # Export components + export_encoder(model, processor, output_path) + has_vision = export_vision_encoder(model, processor, output_path) + export_decoder_init(model, processor, output_path, hidden_size) + export_decoder_with_past(model, processor, output_path, hidden_size) + save_configs(model, processor, output_path, model_id) + + # Summary + logger.info("\n" + "=" * 60) + logger.info("Export complete!") + logger.info("=" * 60) + logger.info(f"\nExported files in {output_dir}:") + for f in sorted(output_path.iterdir()): + if f.is_file(): + size_mb = f.stat().st_size / (1024 * 1024) + logger.info(f" {f.name} ({size_mb:.1f} MB)") + + logger.info(f"\nCapabilities:") + logger.info(" - Text embeddings: encoder.onnx") + if has_vision: + logger.info(" - Image embeddings: vision_encoder.onnx") + logger.info(" - Text generation: encoder.onnx + decoder-init.onnx") + logger.info(" - Decode from embeddings: decoder-init.onnx (with encoder_hidden_states)") + + +def test_exported_model(output_dir: str, test_text: Optional[str] = None) -> None: + """Test the exported ONNX model with sample inputs.""" + import onnxruntime as ort + from transformers import AutoTokenizer + + logger.info("\n" + "=" * 60) + logger.info("Testing exported model...") + logger.info("=" * 60) + + output_path = Path(output_dir) + + # Check required files + encoder_path = output_path / "encoder.onnx" + decoder_init_path = output_path / "decoder-init.onnx" + + if not encoder_path.exists(): + logger.error(f"encoder.onnx not found at {encoder_path}") + return + if not decoder_init_path.exists(): + logger.error(f"decoder-init.onnx not found at {decoder_init_path}") + return + + # Load tokenizer directly (avoid AutoProcessor issues) + tokenizer = AutoTokenizer.from_pretrained(str(output_path)) + + # Create simple processor-like wrapper for compatibility + class SimpleProcessor: + def __init__(self, tokenizer): + self.tokenizer = tokenizer + + processor = SimpleProcessor(tokenizer) + + # Create ONNX Runtime sessions + logger.info("\nLoading ONNX models...") + encoder_session = ort.InferenceSession(str(encoder_path)) + decoder_init_session = ort.InferenceSession(str(decoder_init_path)) + + # Test input + if test_text is None: + test_text = "The capital of France is" + + logger.info(f"\nTest input: {test_text}") + + # Tokenize + inputs = processor.tokenizer( + test_text, + return_tensors="np", + padding=True, + ) + input_ids = inputs["input_ids"].astype(np.int64) + attention_mask = inputs["attention_mask"].astype(np.int64) + + # Run encoder + logger.info("\nRunning encoder...") + encoder_outputs = encoder_session.run( + None, + { + "input_ids": input_ids, + "attention_mask": attention_mask, + }, + ) + encoder_hidden_states = encoder_outputs[0] + logger.info(f"Encoder output shape: {encoder_hidden_states.shape}") + + # Test embeddings (mean pool) + embeddings = np.mean(encoder_hidden_states, axis=1) + logger.info(f"Embedding shape (mean pooled): {embeddings.shape}") + logger.info(f"Embedding sample: {embeddings[0, :5]}...") + + # Run decoder for generation + logger.info("\nRunning decoder (greedy generation)...") + decoder_input_ids = np.array([[processor.tokenizer.pad_token_id or 0]], dtype=np.int64) + + generated_ids = [] + max_new_tokens = 20 + + for step in range(max_new_tokens): + decoder_outputs = decoder_init_session.run( + None, + { + "input_ids": decoder_input_ids, + "encoder_hidden_states": encoder_hidden_states, + "encoder_attention_mask": attention_mask, + }, + ) + logits = decoder_outputs[0] + + # Greedy: take argmax of last token + next_token_id = int(np.argmax(logits[0, -1, :])) + + # Check for EOS + if next_token_id == processor.tokenizer.eos_token_id: + break + + generated_ids.append(next_token_id) + decoder_input_ids = np.concatenate( + [decoder_input_ids, np.array([[next_token_id]], dtype=np.int64)], + axis=1, + ) + + # Decode + generated_text = processor.tokenizer.decode(generated_ids, skip_special_tokens=True) + logger.info(f"\nGenerated: {generated_text}") + + logger.info("\nTest complete!") + + +def main(): + parser = argparse.ArgumentParser( + description="Export T5Gemma-2 models to ONNX for Termite", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Export 270M variant (recommended for development) + ./scripts/export_t5gemma2.py --model google/t5gemma-2-270m-270m --output ./models/t5gemma2/270m + + # Export with testing + ./scripts/export_t5gemma2.py --model google/t5gemma-2-270m-270m --output ./models/t5gemma2/270m --test + + # Export 1B variant + ./scripts/export_t5gemma2.py --model google/t5gemma-2-1b-1b --output ./models/t5gemma2/1b + +Available Models: + - google/t5gemma-2-270m-270m (~800M total params, fast) + - google/t5gemma-2-1b-1b (~2B total params, balanced) + - google/t5gemma-2-4b-4b (~8B total params, best quality) + +Output Files: + - encoder.onnx Text encoder for embeddings & seq2seq + - vision_encoder.onnx SigLIP vision encoder (if multimodal) + - decoder-init.onnx Decoder for first token / all tokens + - decoder.onnx Decoder with KV cache (if supported) + - t5gemma2_config.json Termite-specific configuration + - tokenizer files For text processing + """, + ) + + parser.add_argument( + "--model", + type=str, + default="google/t5gemma-2-270m-270m", + help="HuggingFace model ID (default: google/t5gemma-2-270m-270m)", + ) + parser.add_argument( + "-o", "--output", + type=str, + default="./models/t5gemma2/270m", + help="Output directory for the exported model", + ) + parser.add_argument( + "--test", + action="store_true", + help="Test the exported model after export", + ) + parser.add_argument( + "--test-input", + type=str, + default=None, + help="Custom input text for testing", + ) + + args = parser.parse_args() + + check_dependencies() + + try: + export_model(args.model, args.output) + if args.test: + test_exported_model(args.output, args.test_input) + logger.info("\nAll done!") + except Exception as e: + logger.error(f"\nError: {e}") + import traceback + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + main() From 17d497e3e6c656ffbff3b507317e725cb7a21d2c Mon Sep 17 00:00:00 2001 From: timkaye11 Date: Sat, 3 Jan 2026 19:15:59 -0800 Subject: [PATCH 2/7] termite wip --- docs/T5GEMMA2.md | 13 ++++++++++ pkg/termite/lib/seq2seq/t5gemma2.go | 40 ++++++++++++++++++++--------- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/docs/T5GEMMA2.md b/docs/T5GEMMA2.md index cc2203f..1b6fe6d 100644 --- a/docs/T5GEMMA2.md +++ b/docs/T5GEMMA2.md @@ -250,6 +250,19 @@ T5Gemma-2 requires significant RAM for export: Use `--skip-vision` if you only need text capabilities. +### Out of Memory During Inference + +The 270M model requires significant RAM for inference due to ONNX model sizes: +- **Text embeddings only**: ~2GB RAM (encoder.onnx: 1.07GB + overhead) +- **Text generation (seq2seq)**: ~6GB RAM (encoder + 2x decoder ~4.5GB + overhead) +- **With vision encoder**: Add ~2GB RAM (vision_encoder.onnx: 1.67GB) + +**Memory reduction tips:** +1. **Use text-only mode**: If you don't need images, the vision encoder loads lazily +2. **Limit max_loaded_models**: Set `max_loaded_models: 1` in config +3. **Reduce keep_alive**: Set `keep_alive: 1m` to unload models quickly +4. **Test on a machine with more RAM**: The 270M model needs at least 8GB free RAM + ### Slow Inference Ensure you're using the ONNX build: diff --git a/pkg/termite/lib/seq2seq/t5gemma2.go b/pkg/termite/lib/seq2seq/t5gemma2.go index 4205a45..6564ae0 100644 --- a/pkg/termite/lib/seq2seq/t5gemma2.go +++ b/pkg/termite/lib/seq2seq/t5gemma2.go @@ -84,6 +84,8 @@ type T5Gemma2GeneratorConfig struct { Temperature float32 `json:"temperature"` // TopP is the nucleus sampling probability (used when DoSample=true) TopP float32 `json:"top_p"` + // RepetitionPenalty penalizes repeated tokens (1.0 = no penalty, >1.0 = penalize) + RepetitionPenalty float32 `json:"repetition_penalty"` // ImageSize for vision encoder preprocessing ImageSize int `json:"image_size"` // HiddenSize of the model @@ -159,6 +161,15 @@ func NewT5Gemma2GeneratorWithSession(modelPath string, sharedSession *khugot.Ses pipelineName := fmt.Sprintf("t5gemma2-seq2seq:%s", filepath.Base(modelPath)) pipelineOptions := []khugot.Seq2SeqOption{ pipelines.WithSeq2SeqMaxTokens(config.MaxNewTokens), + // T5Gemma2 models produce a garbage first token due to decoder_start_token_id + // being None in the model config. Skip it in output. + pipelines.WithSkipFirstToken(true), + } + + // Apply repetition penalty if configured (default is 1.2) + if config.RepetitionPenalty > 0 { + pipelineOptions = append(pipelineOptions, + pipelines.WithRepetitionPenalty(config.RepetitionPenalty)) } if config.DoSample && config.TopP > 0 && config.Temperature > 0 { @@ -446,13 +457,14 @@ func (g *T5Gemma2Generator) Close() error { // loadT5Gemma2GeneratorConfig loads T5Gemma-2 generation config from model directory func loadT5Gemma2GeneratorConfig(modelPath string) (T5Gemma2GeneratorConfig, error) { config := T5Gemma2GeneratorConfig{ - MaxNewTokens: 256, - NumBeams: 1, - DoSample: false, - Temperature: 1.0, - TopP: 1.0, - ImageSize: 896, - HiddenSize: 640, + MaxNewTokens: 256, + NumBeams: 1, + DoSample: false, + Temperature: 1.0, + TopP: 1.0, + RepetitionPenalty: 1.2, // Default penalty to avoid degenerate repetition + ImageSize: 896, + HiddenSize: 640, } // Try t5gemma2_config.json first, then config.json @@ -474,11 +486,12 @@ func loadT5Gemma2GeneratorConfig(modelPath string) (T5Gemma2GeneratorConfig, err Task string `json:"task"` HiddenSize int `json:"hidden_size"` GenerationConfig struct { - MaxNewTokens int `json:"max_new_tokens"` - NumBeams int `json:"num_beams"` - DoSample bool `json:"do_sample"` - Temperature float32 `json:"temperature"` - TopP float32 `json:"top_p"` + MaxNewTokens int `json:"max_new_tokens"` + NumBeams int `json:"num_beams"` + DoSample bool `json:"do_sample"` + Temperature float32 `json:"temperature"` + TopP float32 `json:"top_p"` + RepetitionPenalty float32 `json:"repetition_penalty"` } `json:"generation_config"` Encoder struct { VisionConfig struct { @@ -511,6 +524,9 @@ func loadT5Gemma2GeneratorConfig(modelPath string) (T5Gemma2GeneratorConfig, err if rawConfig.GenerationConfig.TopP > 0 { config.TopP = rawConfig.GenerationConfig.TopP } + if rawConfig.GenerationConfig.RepetitionPenalty > 0 { + config.RepetitionPenalty = rawConfig.GenerationConfig.RepetitionPenalty + } if rawConfig.Encoder.VisionConfig.ImageSize > 0 { config.ImageSize = rawConfig.Encoder.VisionConfig.ImageSize From 61ab05a84f4b0b442bad51e01a586ce77e2fd290 Mon Sep 17 00:00:00 2001 From: timkaye11 Date: Sat, 3 Jan 2026 22:21:37 -0800 Subject: [PATCH 3/7] decode api changes --- e2e/t5gemma2_test.go | 155 +++++ openapi.yaml | 306 ++++++++-- pkg/client/client.go | 94 +++ pkg/client/oapi/client.gen.go | 606 ++++++++++++++------ pkg/termite/api.gen.go | 510 ++++++++-------- pkg/termite/api.go | 155 ++++- pkg/termite/lib/seq2seq/decoder.go | 92 +++ pkg/termite/lib/seq2seq/t5gemma2.go | 77 ++- pkg/termite/lib/seq2seq/t5gemma2_decoder.go | 187 ++++++ pkg/termite/lib/seq2seq/t5gemma2_stub.go | 15 + pkg/termite/openapi.yaml | 146 +++++ scripts/export_t5gemma2.py | 71 +++ 12 files changed, 1918 insertions(+), 496 deletions(-) create mode 100644 pkg/termite/lib/seq2seq/decoder.go create mode 100644 pkg/termite/lib/seq2seq/t5gemma2_decoder.go diff --git a/e2e/t5gemma2_test.go b/e2e/t5gemma2_test.go index 4b9149a..efdf9ce 100644 --- a/e2e/t5gemma2_test.go +++ b/e2e/t5gemma2_test.go @@ -369,6 +369,161 @@ func testT5Gemma2RewriteMultiple(t *testing.T, ctx context.Context, c *client.Te } } +// TestT5Gemma2DecodeE2E tests the T5Gemma-2 decode endpoint for embedding-to-text generation. +func TestT5Gemma2DecodeE2E(t *testing.T) { + if testing.Short() { + t.Skip("Skipping E2E test in short mode") + } + + // Check if model exists in rewriters directory (seq2seq models) + modelsDir := getT5Gemma2ModelsDir(t) + if modelsDir == "" { + t.Skip("No models directory available") + } + + rewritersDir := filepath.Join(modelsDir, "rewriters") + modelPath := filepath.Join(rewritersDir, "google", "t5gemma-2-270m") + if !fileExists(filepath.Join(modelPath, "encoder.onnx")) { + t.Skipf("T5Gemma-2 generator model not found at %s. Skipping E2E test.", modelPath) + } + + t.Logf("Using models directory: %s", modelsDir) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + + logger := zaptest.NewLogger(t) + port := findAvailablePort(t) + serverURL := "http://localhost:" + itoa(port) + + config := termite.Config{ + ApiUrl: serverURL, + ModelsDir: modelsDir, + } + + serverCtx, serverCancel := context.WithCancel(ctx) + defer serverCancel() + + readyC := make(chan struct{}) + serverDone := make(chan struct{}) + + go func() { + defer close(serverDone) + termite.RunAsTermite(serverCtx, logger, config, readyC) + }() + + select { + case <-readyC: + t.Log("Server is ready") + case <-time.After(120 * time.Second): + t.Fatal("Timeout waiting for server to be ready") + } + + termiteClient, err := client.NewTermiteClient(serverURL, nil) + require.NoError(t, err, "Creating client failed") + + t.Run("DecodeFromPrompt", func(t *testing.T) { + testT5Gemma2DecodeFromPrompt(t, ctx, termiteClient) + }) + + t.Run("DecodeFromEmbeddings", func(t *testing.T) { + testT5Gemma2DecodeFromEmbeddings(t, ctx, termiteClient) + }) + + t.Run("DecodeValidation", func(t *testing.T) { + testT5Gemma2DecodeValidation(t, ctx, termiteClient) + }) + + serverCancel() + select { + case <-serverDone: + t.Log("Server shutdown complete") + case <-time.After(30 * time.Second): + t.Log("Server shutdown timeout") + } +} + +func testT5Gemma2DecodeFromPrompt(t *testing.T, ctx context.Context, c *client.TermiteClient) { + t.Helper() + + prompt := "The T5Gemma-2 model is a powerful encoder-decoder architecture." + + resp, err := c.DecodeText(ctx, t5Gemma2ModelName, prompt, nil) + require.NoError(t, err, "T5Gemma-2 decode from prompt failed") + require.NotNil(t, resp, "Response should not be nil") + require.Equal(t, t5Gemma2ModelName, resp.Model, "Response model should match") + require.NotEmpty(t, resp.Texts, "Should return generated text") + + t.Logf("Prompt: %q", prompt) + t.Logf("Generated: %q", resp.Texts[0]) +} + +func testT5Gemma2DecodeFromEmbeddings(t *testing.T, ctx context.Context, c *client.TermiteClient) { + t.Helper() + + // First, get embeddings from the embedder endpoint + // T5Gemma-2 returns token-level embeddings (seq_len * hidden_size) + inputs := []string{"Hello, this is a test input for embedding-to-text generation."} + + embeddings, err := c.Embed(ctx, t5Gemma2ModelName, inputs) + require.NoError(t, err, "Getting embeddings failed") + require.Len(t, embeddings, 1, "Should have one embedding") + + // The embedding is flattened: seq_len * hidden_size + // We need to reshape it to [seq_len, hidden_size] for the decode endpoint + flatEmbedding := embeddings[0] + require.NotEmpty(t, flatEmbedding, "Embedding should not be empty") + + // T5Gemma-2 hidden size is 640 + seqLen := len(flatEmbedding) / t5Gemma2HiddenSize + require.Greater(t, seqLen, 0, "Should have at least one token embedding") + t.Logf("Embedding has %d tokens with hidden size %d", seqLen, t5Gemma2HiddenSize) + + // Reshape to [seq_len, hidden_size] + reshapedEmbeddings := make([][]float32, seqLen) + for i := 0; i < seqLen; i++ { + start := i * t5Gemma2HiddenSize + end := start + t5Gemma2HiddenSize + reshapedEmbeddings[i] = flatEmbedding[start:end] + } + + // Try to decode from embeddings + // Note: This may return an error if the decoder isn't fully implemented yet + resp, err := c.DecodeFromEmbeddings(ctx, t5Gemma2ModelName, reshapedEmbeddings, nil) + if err != nil { + // Expected for now - the decoder implementation returns a placeholder error + t.Logf("DecodeFromEmbeddings returned expected error (feature not fully implemented): %v", err) + return + } + + require.NotNil(t, resp, "Response should not be nil") + require.Equal(t, t5Gemma2ModelName, resp.Model, "Response model should match") + require.NotEmpty(t, resp.Texts, "Should return generated text") + + t.Logf("Input: %q", inputs[0]) + t.Logf("Decoded from embeddings: %q", resp.Texts[0]) +} + +func testT5Gemma2DecodeValidation(t *testing.T, ctx context.Context, c *client.TermiteClient) { + t.Helper() + + // Test that we get proper errors for invalid requests + + // Test: no prompt or embeddings + prompt := "" + _, err := c.DecodeText(ctx, t5Gemma2ModelName, prompt, nil) + require.Error(t, err, "Should fail when prompt is empty") + assert.Contains(t, err.Error(), "bad request", "Should be a bad request error") + + // Test: wrong embedding dimensions + invalidEmbeddings := [][]float32{ + {1.0, 2.0, 3.0}, // Wrong size - should be 640 + } + _, err = c.DecodeFromEmbeddings(ctx, t5Gemma2ModelName, invalidEmbeddings, nil) + require.Error(t, err, "Should fail with wrong embedding dimensions") + t.Logf("Invalid dimensions error: %v", err) +} + // Helper functions func fileExists(path string) bool { _, err := os.Stat(path) diff --git a/openapi.yaml b/openapi.yaml index c5db817..621c519 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -73,7 +73,7 @@ paths: Models are auto-discovered from `models_dir/embedders/` at startup. Use the `/api/models` endpoint to list available models. - - **Text-only models** (e.g., bge-small-en-v1.5): Accept text strings + - **Text-only models** (e.g., BAAI/bge-small-en-v1.5): Accept text strings - **Multimodal models** (e.g., CLIP): Accept text and images via data URIs ## Input Formats @@ -99,7 +99,7 @@ paths: Text embedding (Ollama-compatible): ```json { - "model": "bge-small-en-v1.5", + "model": "BAAI/bge-small-en-v1.5", "input": ["hello world", "machine learning"] } ``` @@ -107,7 +107,7 @@ paths: Multimodal embedding (OpenAI-compatible): ```json { - "model": "clip-vit-base-patch32", + "model": "openai/clip-vit-base-patch32", "input": [ {"type": "text", "text": "a photo of a cat"}, {"type": "image_url", "image_url": {"url": "data:image/png;base64,iVBORw0..."}} @@ -237,7 +237,7 @@ paths: ```json { - "model": "bge-reranker-v2-m3", + "model": "BAAI/bge-reranker-v2-m3", "query": "machine learning applications", "prompts": [ "Introduction to Machine Learning: This guide covers...", @@ -309,7 +309,7 @@ paths: Uses OpenAI-compatible chat format with messages array: ```json { - "model": "gemma-3-1b-it", + "model": "google/gemma-3-1b-it", "messages": [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Hello!"} @@ -325,7 +325,7 @@ paths: curl -X POST http://localhost:8080/api/generate \ -H "Content-Type: application/json" \ -d '{ - "model": "gemma-3-1b-it", + "model": "google/gemma-3-1b-it", "messages": [{"role": "user", "content": "What is machine learning?"}], "max_tokens": 100 }' @@ -337,7 +337,7 @@ paths: curl -X POST http://localhost:8080/api/generate \ -H "Content-Type: application/json" \ -d '{ - "model": "gemma-3-1b-it", + "model": "google/gemma-3-1b-it", "messages": [{"role": "user", "content": "Hello!"}], "stream": true }' @@ -410,7 +410,7 @@ paths: ```json { - "model": "bert-base-ner", + "model": "dslim/bert-base-NER", "texts": ["John Smith works at Google.", "Apple Inc. is in Cupertino."] } ``` @@ -476,7 +476,7 @@ paths: For question generation with LMQG models: ```json { - "model": "flan-t5-small-squad-qg", + "model": "lmqg/flan-t5-small-squad-qg", "inputs": ["generate question: Beyonce Beyonce starred as Etta James in Cadillac Records."] } ``` @@ -520,10 +520,10 @@ paths: $ref: '#/components/schemas/Error' /decode: post: - summary: Decode text using T5Gemma-2 decoder + summary: Decode text from embeddings using T5Gemma-2 description: | - Generate text using the T5Gemma-2 decoder for autoregressive text generation. - This is a novel capability that uses the decoder component of encoder-decoder models. + Generate text using the T5Gemma-2 decoder from pre-computed embeddings or text prompts. + This enables embedding-to-text generation for custom embedding injection workflows. ## Models @@ -533,10 +533,11 @@ paths: ## Use Cases - **Text Continuation**: Generate text continuations from prompts - - **Embedding Inversion**: Reconstruct text from embeddings (experimental) - - **Guided Generation**: Generate text conditioned on encoder outputs + - **Embedding Inversion**: Reconstruct text from embeddings + - **Guided Generation**: Generate text conditioned on custom encoder outputs + - **Cross-modal Generation**: Generate text from vision embeddings - ## Example + ## Example (with prompt) ```json { @@ -544,6 +545,15 @@ paths: "prompt": "The meaning of life is" } ``` + + ## Example (with embeddings) + + ```json + { + "model": "google/t5gemma-2-270m", + "embeddings": [[0.1, 0.2, ...], [0.3, 0.4, ...]] + } + ``` operationId: decodeText requestBody: required: true @@ -718,7 +728,7 @@ components: model: type: string description: Name of the embedder model from models_dir/embedders/ - example: bge-small-en-v1.5 + example: BAAI/bge-small-en-v1.5 input: oneOf: - type: string @@ -746,7 +756,7 @@ components: EmbedResponse: type: object example: - model: bge-small-en-v1.5 + model: BAAI/bge-small-en-v1.5 embeddings: - - 0.0123 - -0.0456 @@ -761,7 +771,7 @@ components: model: type: string description: Model used for embedding - example: bge-small-en-v1.5 + example: BAAI/bge-small-en-v1.5 embeddings: type: array items: @@ -886,7 +896,7 @@ components: model: type: string description: Name of reranking model from models_dir/rerankers/ - example: bge-reranker-v2-m3 + example: BAAI/bge-reranker-v2-m3 query: type: string description: Search query for relevance scoring @@ -955,7 +965,7 @@ components: model: type: string description: Name of recognizer model from models_dir/recognizers/ - example: bert-base-ner + example: dslim/bert-base-NER texts: type: array items: @@ -1047,7 +1057,7 @@ components: model: type: string description: Name of Seq2Seq rewriter model from models_dir/rewriters/ - example: flan-t5-small + example: lmqg/flan-t5-small-squad-qg inputs: type: array items: @@ -1075,7 +1085,6 @@ components: type: object required: - model - - prompt properties: model: type: string @@ -1083,8 +1092,28 @@ components: example: google/t5gemma-2-270m prompt: type: string - description: Input prompt for text generation - example: "The meaning of life is" + description: Input prompt for text generation (mutually exclusive with embeddings) + example: The meaning of life is + embeddings: + type: array + items: + type: array + items: + type: number + format: float + description: | + Pre-computed encoder hidden states for embedding-to-text generation. + Shape: [sequence_length, hidden_size] for single input. + When provided, the decoder generates text directly from these embeddings + without running the encoder. Mutually exclusive with 'prompt'. + sequence_length: + type: integer + description: The sequence length of the embeddings (for reshaping flat arrays) + attention_mask: + type: array + items: + type: integer + description: Optional attention mask for embeddings. Shape [sequence_length]. max_tokens: type: integer description: Maximum number of tokens to generate (default 256) @@ -1093,12 +1122,12 @@ components: type: number format: float description: Sampling temperature (default 1.0) - default: 1.0 + default: 1 top_p: type: number format: float description: Nucleus sampling probability (default 1.0) - default: 1.0 + default: 1 DecodeResponse: type: object required: @@ -1137,55 +1166,55 @@ components: description: Available chunking models (always includes "fixed") example: - fixed - - chonky-mmbert-small-multilingual-1 + - mirth/chonky-mmbert-small-multilingual-1 rerankers: type: array items: type: string description: Available reranking models example: - - bge-reranker-v2-m3 + - BAAI/bge-reranker-v2-m3 embedders: type: array items: type: string description: Available embedding models from models_dir/embedders/ example: - - bge-small-en-v1.5 - - bge-small-en-v1.5-i8-qt + - BAAI/bge-small-en-v1.5 + - BAAI/bge-small-en-v1.5:i8 generators: type: array items: type: string description: Available generator/LLM models from models_dir/generators/ example: - - gemma-3-1b-it - - tiny-random-gemma-3 + - google/gemma-3-1b-it + - onnxruntime/Gemma-3-ONNX recognizers: type: array items: type: string description: Available recognizer models from models_dir/recognizers/ example: - - bert-base-ner - - distilbert-ner - - gliner-small + - dslim/bert-base-NER + - dslim/bert-large-NER + - onnx-community/gliner_small-v2.1 extractors: type: array items: type: string description: Available GLiNER extractor models (zero-shot recognition with custom labels) example: - - gliner-small - - gliner-multitask + - onnx-community/gliner_small-v2.1 + - onnx-community/gliner-multitask rewriters: type: array items: type: string description: Available Seq2Seq rewriter models from models_dir/rewriters/ example: - - flan-t5-small - - lmqg-question-generation + - lmqg/flan-t5-small-squad-qg + - lmqg/flan-t5-base-squad-qg recognizer_info: type: object additionalProperties: @@ -1195,14 +1224,14 @@ components: Map of model name to model info. Use this to determine what capabilities each recognizer supports (labels, zeroshot, relations, answers). example: - bert-base-ner: + dslim/bert-base-NER: capabilities: - labels - gliner-small: + onnx-community/gliner_small-v2.1: capabilities: - labels - zeroshot - gliner-multitask: + onnx-community/gliner-multitask: capabilities: - labels - zeroshot @@ -1233,12 +1262,122 @@ components: example: - labels - zeroshot + Tool: + type: object + description: A tool (function) that the model can call + required: + - type + - function + properties: + type: + type: string + enum: + - function + description: The type of tool (currently only "function" is supported) + function: + $ref: '#/components/schemas/FunctionDefinition' + FunctionDefinition: + type: object + description: Definition of a function that can be called by the model + required: + - name + properties: + name: + type: string + description: The name of the function to call + example: get_weather + description: + type: string + description: A description of what the function does + example: Get the current weather in a location + parameters: + type: object + additionalProperties: true + description: JSON Schema object describing the function parameters + example: + type: object + properties: + location: + type: string + description: The city and state, e.g. San Francisco, CA + required: + - location + strict: + type: boolean + description: Whether to enforce strict parameter validation + default: false + ToolCall: + type: object + description: A tool call made by the model + required: + - id + - type + - function + properties: + id: + type: string + description: Unique identifier for this tool call + example: call_abc123 + type: + type: string + enum: + - function + description: The type of tool call (currently only "function") + function: + $ref: '#/components/schemas/ToolCallFunction' + ToolCallFunction: + type: object + description: The function called by the model + required: + - name + - arguments + properties: + name: + type: string + description: The name of the function called + example: get_weather + arguments: + type: string + description: JSON string of the arguments to the function + example: '{"location": "San Francisco, CA"}' + ToolChoice: + description: | + Controls how the model uses tools. Options: + - "auto": Model decides whether to call a tool (default) + - "none": Model will not call any tools + - "required": Model must call at least one tool + - object: Force a specific function to be called + oneOf: + - type: string + enum: + - auto + - none + - required + - type: object + description: Force a specific function to be called + required: + - type + - function + properties: + type: + type: string + enum: + - function + function: + type: object + required: + - name + properties: + name: + type: string + description: The name of the function to call Role: type: string enum: - system - user - assistant + - tool description: The role of a message sender in a conversation FinishReason: type: string @@ -1265,12 +1404,19 @@ components: type: object required: - role - - content properties: role: $ref: '#/components/schemas/Role' content: $ref: '#/components/schemas/ChatMessageContent' + tool_calls: + type: array + items: + $ref: '#/components/schemas/ToolCall' + description: Tool calls made by the assistant (only for role=assistant) + tool_call_id: + type: string + description: ID of the tool call this message is responding to (only for role=tool) GenerateRequest: type: object required: @@ -1280,7 +1426,7 @@ components: model: type: string description: Name of the generator model from models_dir/generators/ - example: gemma-3-1b-it + example: google/gemma-3-1b-it messages: type: array items: @@ -1310,6 +1456,16 @@ components: type: boolean description: If true, partial message deltas will be sent as SSE events default: false + tools: + type: array + items: + $ref: '#/components/schemas/Tool' + description: | + List of tools (functions) the model can call. + Only supported by models with tool_call_format configured. + tool_choice: + $ref: '#/components/schemas/ToolChoice' + description: Controls how the model uses tools GenerateResponse: type: object description: OpenAI-compatible chat completion response @@ -1366,13 +1522,18 @@ components: type: object required: - role - - content properties: role: $ref: '#/components/schemas/Role' content: type: string - description: The generated message content + nullable: true + description: The generated message content (null when tool_calls is present) + tool_calls: + type: array + items: + $ref: '#/components/schemas/ToolCall' + description: Tool calls made by the model (only present when finish_reason is tool_calls) GenerateUsage: type: object required: @@ -1435,6 +1596,38 @@ components: type: string nullable: true description: Token content delta + tool_calls: + type: array + items: + $ref: '#/components/schemas/ToolCallDelta' + description: Tool call deltas for streaming tool calls + ToolCallDelta: + type: object + description: Incremental tool call data for streaming + properties: + index: + type: integer + description: Index of the tool call in the array + id: + type: string + description: Unique identifier (only in first delta for this index) + type: + type: string + enum: + - function + description: The type of tool call (only in first delta) + function: + $ref: '#/components/schemas/ToolCallFunctionDelta' + ToolCallFunctionDelta: + type: object + description: Incremental function call data for streaming + properties: + name: + type: string + description: Function name (only in first delta) + arguments: + type: string + description: Incremental arguments JSON string Config: type: object required: @@ -1468,9 +1661,10 @@ components: description: | How long to keep models loaded in memory after last use (Ollama-compatible). Models are automatically unloaded after this duration of inactivity. - Use Go duration format: "5m" (5 minutes), "1h" (1 hour), "0" (never unload, eager loading). - When set to "0" or omitted, models are loaded eagerly at startup and never unloaded (legacy behavior). - default: '0' + Use Go duration format: "5m" (5 minutes), "1h" (1 hour), "0" (eager loading). + Defaults to "5m" (lazy loading) like Ollama. Set to "0" to explicitly enable eager loading + where all models are loaded at startup and never unloaded. + default: 5m example: 5m max_loaded_models: type: integer @@ -1548,11 +1742,11 @@ components: description: | List of model names to preload at startup (Ollama-compatible). These models are loaded immediately when Termite starts, avoiding first-request latency. - Model names should match those in models_dir/embedders/ (e.g., "bge-small-en-v1.5"). + Model names should match those in models_dir/embedders/ (e.g., "BAAI/bge-small-en-v1.5"). Only effective when keep_alive is non-zero (lazy loading mode). example: - - bge-small-en-v1.5 - - clip-vit-base-patch32 + - BAAI/bge-small-en-v1.5 + - openai/clip-vit-base-patch32 max_memory_mb: type: integer description: | @@ -1574,8 +1768,8 @@ components: description: | Per-model loading strategy overrides. Maps model names to their loading strategy. Models not in this map use the default strategy based on keep_alive: - - If keep_alive="0" (default): eager loading (load at startup, never unload) - - If keep_alive>0: lazy loading (load on demand, unload after idle) + - If keep_alive>0 (default "5m"): lazy loading (load on demand, unload after idle) + - If keep_alive="0": eager loading (load at startup, never unload) When a model has strategy "eager" in this map: - It is loaded at startup (as part of preload) @@ -1583,8 +1777,8 @@ components: This allows mixing eager and lazy models in the same pool. example: - bge-small-en-v1.5: eager - chonky: lazy + BAAI/bge-small-en-v1.5: eager + mirth/chonky-mmbert-small-multilingual-1: lazy log: $ref: '#/components/schemas/schemas-Config' VersionResponse: diff --git a/pkg/client/client.go b/pkg/client/client.go index b1bf1d6..8cbd5f0 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -421,6 +421,100 @@ func (c *TermiteClient) RewriteText(ctx context.Context, model string, inputs [] return resp.JSON200, nil } +// DecodeConfig contains configuration for text decoding/generation. +type DecodeConfig struct { + MaxTokens int + Temperature float32 + TopP float32 +} + +// DecodeText decodes text from a prompt using a Seq2Seq model. +func (c *TermiteClient) DecodeText(ctx context.Context, model string, prompt string, cfg *DecodeConfig) (*oapi.DecodeResponse, error) { + req := oapi.DecodeRequest{ + Model: model, + Prompt: prompt, + } + + if cfg != nil { + if cfg.MaxTokens > 0 { + req.MaxTokens = cfg.MaxTokens + } + if cfg.Temperature > 0 { + req.Temperature = cfg.Temperature + } + if cfg.TopP > 0 { + req.TopP = cfg.TopP + } + } + + resp, err := c.client.DecodeTextWithResponse(ctx, req) + if err != nil { + return nil, fmt.Errorf("sending request: %w", err) + } + + if resp.JSON400 != nil { + return nil, fmt.Errorf("bad request: %s", resp.JSON400.Error) + } + if resp.JSON404 != nil { + return nil, fmt.Errorf("model not found: %s", resp.JSON404.Error) + } + if resp.JSON500 != nil { + return nil, fmt.Errorf("server error: %s", resp.JSON500.Error) + } + if resp.JSON503 != nil { + return nil, fmt.Errorf("service unavailable: %s", resp.JSON503.Error) + } + if resp.JSON200 == nil { + return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode(), string(resp.Body)) + } + + return resp.JSON200, nil +} + +// DecodeFromEmbeddings generates text from pre-computed encoder hidden states. +// This enables embedding-to-text generation by bypassing the encoder. +func (c *TermiteClient) DecodeFromEmbeddings(ctx context.Context, model string, embeddings [][]float32, cfg *DecodeConfig) (*oapi.DecodeResponse, error) { + req := oapi.DecodeRequest{ + Model: model, + Embeddings: embeddings, + } + + if cfg != nil { + if cfg.MaxTokens > 0 { + req.MaxTokens = cfg.MaxTokens + } + if cfg.Temperature > 0 { + req.Temperature = cfg.Temperature + } + if cfg.TopP > 0 { + req.TopP = cfg.TopP + } + } + + resp, err := c.client.DecodeTextWithResponse(ctx, req) + if err != nil { + return nil, fmt.Errorf("sending request: %w", err) + } + + if resp.JSON400 != nil { + return nil, fmt.Errorf("bad request: %s", resp.JSON400.Error) + } + if resp.JSON404 != nil { + return nil, fmt.Errorf("model not found: %s", resp.JSON404.Error) + } + if resp.JSON500 != nil { + return nil, fmt.Errorf("server error: %s", resp.JSON500.Error) + } + if resp.JSON503 != nil { + return nil, fmt.Errorf("service unavailable: %s", resp.JSON503.Error) + } + if resp.JSON200 == nil { + return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode(), string(resp.Body)) + } + + return resp.JSON200, nil +} + // GenerateConfig contains configuration for text generation. type GenerateConfig struct { MaxTokens int diff --git a/pkg/client/oapi/client.gen.go b/pkg/client/oapi/client.gen.go index 917acc0..84d1604 100644 --- a/pkg/client/oapi/client.gen.go +++ b/pkg/client/oapi/client.gen.go @@ -287,6 +287,48 @@ type ContentPart struct { union json.RawMessage } +// DecodeRequest defines model for DecodeRequest. +type DecodeRequest struct { + // AttentionMask Optional attention mask for embeddings. Shape [sequence_length]. + AttentionMask []int `json:"attention_mask,omitempty,omitzero"` + + // Embeddings Pre-computed encoder hidden states for embedding-to-text generation. + // Shape: [sequence_length, hidden_size] for single input. + // When provided, the decoder generates text directly from these embeddings + // without running the encoder. Mutually exclusive with 'prompt'. + Embeddings [][]float32 `json:"embeddings,omitempty,omitzero"` + + // MaxTokens Maximum number of tokens to generate (default 256) + MaxTokens int `json:"max_tokens,omitempty,omitzero"` + + // Model Name of T5Gemma-2 or compatible decoder model + Model string `json:"model"` + + // Prompt Input prompt for text generation (mutually exclusive with embeddings) + Prompt string `json:"prompt,omitempty,omitzero"` + + // SequenceLength The sequence length of the embeddings (for reshaping flat arrays) + SequenceLength int `json:"sequence_length,omitempty,omitzero"` + + // Temperature Sampling temperature (default 1.0) + Temperature float32 `json:"temperature,omitempty,omitzero"` + + // TopP Nucleus sampling probability (default 1.0) + TopP float32 `json:"top_p,omitempty,omitzero"` +} + +// DecodeResponse defines model for DecodeResponse. +type DecodeResponse struct { + // Model Name of model used for decoding + Model string `json:"model"` + + // Texts Generated text sequences + Texts []string `json:"texts"` + + // Tokens Token IDs for each generated sequence (optional) + Tokens [][]int `json:"tokens,omitempty,omitzero"` +} + // EmbedRequest defines model for EmbedRequest. type EmbedRequest struct { // Input Input content to embed. Supports three formats: @@ -759,6 +801,9 @@ type VersionResponse struct { // ChunkTextJSONRequestBody defines body for ChunkText for application/json ContentType. type ChunkTextJSONRequestBody = ChunkRequest +// DecodeTextJSONRequestBody defines body for DecodeText for application/json ContentType. +type DecodeTextJSONRequestBody = DecodeRequest + // GenerateEmbeddingsJSONRequestBody defines body for GenerateEmbeddings for application/json ContentType. type GenerateEmbeddingsJSONRequestBody = EmbedRequest @@ -1126,6 +1171,11 @@ type ClientInterface interface { ChunkText(ctx context.Context, body ChunkTextJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // DecodeTextWithBody request with any body + DecodeTextWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + DecodeText(ctx context.Context, body DecodeTextJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // GenerateEmbeddingsWithBody request with any body GenerateEmbeddingsWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1182,6 +1232,30 @@ func (c *Client) ChunkText(ctx context.Context, body ChunkTextJSONRequestBody, r return c.Client.Do(req) } +func (c *Client) DecodeTextWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDecodeTextRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) DecodeText(ctx context.Context, body DecodeTextJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDecodeTextRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) GenerateEmbeddingsWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewGenerateEmbeddingsRequestWithBody(c.Server, contentType, body) if err != nil { @@ -1366,6 +1440,46 @@ func NewChunkTextRequestWithBody(server string, contentType string, body io.Read return req, nil } +// NewDecodeTextRequest calls the generic DecodeText builder with application/json body +func NewDecodeTextRequest(server string, body DecodeTextJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewDecodeTextRequestWithBody(server, "application/json", bodyReader) +} + +// NewDecodeTextRequestWithBody generates requests for DecodeText with any type of body +func NewDecodeTextRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/decode") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + // NewGenerateEmbeddingsRequest calls the generic GenerateEmbeddings builder with application/json body func NewGenerateEmbeddingsRequest(server string, body GenerateEmbeddingsJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -1668,6 +1782,11 @@ type ClientWithResponsesInterface interface { ChunkTextWithResponse(ctx context.Context, body ChunkTextJSONRequestBody, reqEditors ...RequestEditorFn) (*ChunkTextResponse, error) + // DecodeTextWithBodyWithResponse request with any body + DecodeTextWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*DecodeTextResponse, error) + + DecodeTextWithResponse(ctx context.Context, body DecodeTextJSONRequestBody, reqEditors ...RequestEditorFn) (*DecodeTextResponse, error) + // GenerateEmbeddingsWithBodyWithResponse request with any body GenerateEmbeddingsWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*GenerateEmbeddingsResponse, error) @@ -1724,6 +1843,32 @@ func (r ChunkTextResponse) StatusCode() int { return 0 } +type DecodeTextResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *DecodeResponse + JSON400 *Error + JSON404 *Error + JSON500 *Error + JSON503 *Error +} + +// Status returns HTTPResponse.Status +func (r DecodeTextResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r DecodeTextResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type GenerateEmbeddingsResponse struct { Body []byte HTTPResponse *http.Response @@ -1917,6 +2062,23 @@ func (c *ClientWithResponses) ChunkTextWithResponse(ctx context.Context, body Ch return ParseChunkTextResponse(rsp) } +// DecodeTextWithBodyWithResponse request with arbitrary body returning *DecodeTextResponse +func (c *ClientWithResponses) DecodeTextWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*DecodeTextResponse, error) { + rsp, err := c.DecodeTextWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseDecodeTextResponse(rsp) +} + +func (c *ClientWithResponses) DecodeTextWithResponse(ctx context.Context, body DecodeTextJSONRequestBody, reqEditors ...RequestEditorFn) (*DecodeTextResponse, error) { + rsp, err := c.DecodeText(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseDecodeTextResponse(rsp) +} + // GenerateEmbeddingsWithBodyWithResponse request with arbitrary body returning *GenerateEmbeddingsResponse func (c *ClientWithResponses) GenerateEmbeddingsWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*GenerateEmbeddingsResponse, error) { rsp, err := c.GenerateEmbeddingsWithBody(ctx, contentType, body, reqEditors...) @@ -2060,6 +2222,60 @@ func ParseChunkTextResponse(rsp *http.Response) (*ChunkTextResponse, error) { return response, nil } +// ParseDecodeTextResponse parses an HTTP response from a DecodeTextWithResponse call +func ParseDecodeTextResponse(rsp *http.Response) (*DecodeTextResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &DecodeTextResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest DecodeResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 503: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON503 = &dest + + } + + return response, nil +} + // ParseGenerateEmbeddingsResponse parses an HTTP response from a GenerateEmbeddingsWithResponse call func ParseGenerateEmbeddingsResponse(rsp *http.Response) (*GenerateEmbeddingsResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -2405,196 +2621,206 @@ func ParseGetVersionResponse(rsp *http.Response) (*GetVersionResponse, error) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "swAA", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/pkg/termite/api.gen.go b/pkg/termite/api.gen.go index 5a5d5ef..95eae57 100644 --- a/pkg/termite/api.gen.go +++ b/pkg/termite/api.gen.go @@ -287,6 +287,48 @@ type ContentPart struct { union json.RawMessage } +// DecodeRequest defines model for DecodeRequest. +type DecodeRequest struct { + // AttentionMask Optional attention mask for embeddings. Shape [sequence_length]. + AttentionMask []int `json:"attention_mask,omitempty,omitzero"` + + // Embeddings Pre-computed encoder hidden states for embedding-to-text generation. + // Shape: [sequence_length, hidden_size] for single input. + // When provided, the decoder generates text directly from these embeddings + // without running the encoder. Mutually exclusive with 'prompt'. + Embeddings [][]float32 `json:"embeddings,omitempty,omitzero"` + + // MaxTokens Maximum number of tokens to generate (default 256) + MaxTokens int `json:"max_tokens,omitempty,omitzero"` + + // Model Name of T5Gemma-2 or compatible decoder model + Model string `json:"model"` + + // Prompt Input prompt for text generation (mutually exclusive with embeddings) + Prompt string `json:"prompt,omitempty,omitzero"` + + // SequenceLength The sequence length of the embeddings (for reshaping flat arrays) + SequenceLength int `json:"sequence_length,omitempty,omitzero"` + + // Temperature Sampling temperature (default 1.0) + Temperature float32 `json:"temperature,omitempty,omitzero"` + + // TopP Nucleus sampling probability (default 1.0) + TopP float32 `json:"top_p,omitempty,omitzero"` +} + +// DecodeResponse defines model for DecodeResponse. +type DecodeResponse struct { + // Model Name of model used for decoding + Model string `json:"model"` + + // Texts Generated text sequences + Texts []string `json:"texts"` + + // Tokens Token IDs for each generated sequence (optional) + Tokens [][]int `json:"tokens,omitempty,omitzero"` +} + // EmbedRequest defines model for EmbedRequest. type EmbedRequest struct { // Input Input content to embed. Supports three formats: @@ -641,45 +683,6 @@ type RewriteResponse struct { Texts [][]string `json:"texts"` } -// DecodeRequest defines model for DecodeRequest. -type DecodeRequest struct { - // Model Name of T5Gemma-2 or compatible decoder model - Model string `json:"model"` - - // Prompt Input prompt for text generation (mutually exclusive with Embeddings) - Prompt string `json:"prompt,omitempty"` - - // Embeddings Pre-computed encoder hidden states for embedding inversion - // Shape: [sequence_length, hidden_size] or [batch, sequence_length, hidden_size] - // When provided, the decoder generates text directly from these embeddings - // without running the encoder - Embeddings [][]float32 `json:"embeddings,omitempty"` - - // SequenceLength The sequence length of the embeddings (for reshaping flat arrays) - SequenceLength *int `json:"sequence_length,omitempty"` - - // MaxTokens Maximum number of tokens to generate (default 256) - MaxTokens *int `json:"max_tokens,omitempty"` - - // Temperature Sampling temperature (default 1.0) - Temperature *float32 `json:"temperature,omitempty"` - - // TopP Nucleus sampling probability (default 1.0) - TopP *float32 `json:"top_p,omitempty"` -} - -// DecodeResponse defines model for DecodeResponse. -type DecodeResponse struct { - // Model Name of model used for decoding - Model string `json:"model"` - - // Texts Generated text sequences - Texts []string `json:"texts"` - - // Tokens Token IDs for each generated sequence (optional) - Tokens [][]int `json:"tokens,omitempty"` -} - // Role The role of a message sender in a conversation type Role string @@ -798,6 +801,9 @@ type VersionResponse struct { // ChunkTextJSONRequestBody defines body for ChunkText for application/json ContentType. type ChunkTextJSONRequestBody = ChunkRequest +// DecodeTextJSONRequestBody defines body for DecodeText for application/json ContentType. +type DecodeTextJSONRequestBody = DecodeRequest + // GenerateEmbeddingsJSONRequestBody defines body for GenerateEmbeddings for application/json ContentType. type GenerateEmbeddingsJSONRequestBody = EmbedRequest @@ -1092,6 +1098,9 @@ type ServerInterface interface { // Chunk text into smaller segments // (POST /chunk) ChunkText(w http.ResponseWriter, r *http.Request) + // Decode text from embeddings using T5Gemma-2 + // (POST /decode) + DecodeText(w http.ResponseWriter, r *http.Request) // Generate embeddings // (POST /embed) GenerateEmbeddings(w http.ResponseWriter, r *http.Request) @@ -1110,9 +1119,6 @@ type ServerInterface interface { // Rewrite text using Seq2Seq models // (POST /rewrite) RewriteText(w http.ResponseWriter, r *http.Request) - // Decode text using T5Gemma-2 decoder - // (POST /decode) - DecodeText(w http.ResponseWriter, r *http.Request) // Get version information // (GET /version) GetVersion(w http.ResponseWriter, r *http.Request) @@ -1141,6 +1147,20 @@ func (siw *ServerInterfaceWrapper) ChunkText(w http.ResponseWriter, r *http.Requ handler.ServeHTTP(w, r) } +// DecodeText operation middleware +func (siw *ServerInterfaceWrapper) DecodeText(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.DecodeText(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // GenerateEmbeddings operation middleware func (siw *ServerInterfaceWrapper) GenerateEmbeddings(w http.ResponseWriter, r *http.Request) { @@ -1225,20 +1245,6 @@ func (siw *ServerInterfaceWrapper) RewriteText(w http.ResponseWriter, r *http.Re handler.ServeHTTP(w, r) } -// DecodeText operation middleware -func (siw *ServerInterfaceWrapper) DecodeText(w http.ResponseWriter, r *http.Request) { - - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.DecodeText(w, r) - })) - - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) -} - // GetVersion operation middleware func (siw *ServerInterfaceWrapper) GetVersion(w http.ResponseWriter, r *http.Request) { @@ -1374,13 +1380,13 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H } m.HandleFunc("POST "+options.BaseURL+"/chunk", wrapper.ChunkText) + m.HandleFunc("POST "+options.BaseURL+"/decode", wrapper.DecodeText) m.HandleFunc("POST "+options.BaseURL+"/embed", wrapper.GenerateEmbeddings) m.HandleFunc("POST "+options.BaseURL+"/generate", wrapper.GenerateContent) m.HandleFunc("GET "+options.BaseURL+"/models", wrapper.ListModels) m.HandleFunc("POST "+options.BaseURL+"/recognize", wrapper.RecognizeEntities) m.HandleFunc("POST "+options.BaseURL+"/rerank", wrapper.RerankPrompts) m.HandleFunc("POST "+options.BaseURL+"/rewrite", wrapper.RewriteText) - m.HandleFunc("POST "+options.BaseURL+"/decode", wrapper.DecodeText) m.HandleFunc("GET "+options.BaseURL+"/version", wrapper.GetVersion) return m @@ -1389,196 +1395,206 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "swAA", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/pkg/termite/api.go b/pkg/termite/api.go index cc3e97c..6480b82 100644 --- a/pkg/termite/api.go +++ b/pkg/termite/api.go @@ -34,6 +34,7 @@ import ( "github.com/antflydb/antfly-go/libaf/scraping" "github.com/antflydb/termite/pkg/termite/lib/generation" "github.com/antflydb/termite/pkg/termite/lib/ner" + "github.com/antflydb/termite/pkg/termite/lib/seq2seq" "github.com/bytedance/sonic/decoder" "github.com/bytedance/sonic/encoder" "go.uber.org/zap" @@ -89,9 +90,9 @@ func (t *TermiteAPI) RewriteText(w http.ResponseWriter, r *http.Request) { t.node.handleApiRewrite(w, r) } -// DecodeText implements ServerInterface - placeholder for future embedding inversion feature +// DecodeText implements ServerInterface - embedding-to-text generation func (t *TermiteAPI) DecodeText(w http.ResponseWriter, r *http.Request) { - http.Error(w, "decode endpoint not yet implemented; use /api/rewrite for text generation", http.StatusNotImplemented) + t.node.handleApiDecode(w, r) } // ListModels implements ServerInterface @@ -1348,3 +1349,153 @@ func (ln *TermiteNode) handleApiRewrite(w http.ResponseWriter, r *http.Request) } } +// handleApiDecode handles POST /api/decode for embedding-to-text generation. +// It supports two modes: +// - Text prompt: standard text generation using the seq2seq model +// - Embeddings: direct decoding from pre-computed encoder hidden states +func (ln *TermiteNode) handleApiDecode(w http.ResponseWriter, r *http.Request) { + defer func() { _ = r.Body.Close() }() + + // Check if seq2seq registry is available + if ln.seq2seqRegistry == nil { + http.Error(w, "decode not available: seq2seq models not loaded", http.StatusServiceUnavailable) + return + } + + // Apply backpressure + release, err := ln.requestQueue.Acquire(r.Context()) + if err != nil { + ln.logger.Error("failed to acquire request slot", zap.Error(err)) + http.Error(w, "service overloaded", http.StatusServiceUnavailable) + return + } + defer release() + + // Decode request + var req DecodeRequest + if err := decoder.NewStreamDecoder(r.Body).Decode(&req); err != nil { + ln.logger.Error("decoding decode request", zap.Error(err)) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // Validate request + if req.Model == "" { + http.Error(w, "model is required", http.StatusBadRequest) + return + } + + // Validate: prompt XOR embeddings + hasPrompt := req.Prompt != "" + hasEmbeddings := len(req.Embeddings) > 0 + if !hasPrompt && !hasEmbeddings { + http.Error(w, "either prompt or embeddings is required", http.StatusBadRequest) + return + } + if hasPrompt && hasEmbeddings { + http.Error(w, "prompt and embeddings are mutually exclusive", http.StatusBadRequest) + return + } + + // Get model from registry + model, err := ln.seq2seqRegistry.Get(req.Model) + if err != nil { + ln.logger.Error("failed to get decode model", + zap.String("model", req.Model), + zap.Error(err)) + http.Error(w, fmt.Sprintf("model not found: %s", req.Model), http.StatusNotFound) + return + } + + var output *seq2seq.GeneratedOutput + + if hasEmbeddings { + // Embedding-to-text path: decode from pre-computed embeddings + dec, ok := model.(seq2seq.Decoder) + if !ok { + http.Error(w, "model does not support embedding decoding", http.StatusBadRequest) + return + } + + hiddenSize := dec.HiddenSize() + + // Validate embedding dimensions + for i, emb := range req.Embeddings { + if len(emb) != hiddenSize { + http.Error(w, fmt.Sprintf("embedding %d has wrong dimension: got %d, expected %d", + i, len(emb), hiddenSize), http.StatusBadRequest) + return + } + } + + // Build decode options + opts := seq2seq.DefaultDecodeOptions() + if req.MaxTokens > 0 { + opts.MaxTokens = req.MaxTokens + } + if req.Temperature > 0 { + opts.Temperature = req.Temperature + } + if req.TopP > 0 { + opts.TopP = req.TopP + } + + // Build decoder input + input := &seq2seq.DecoderInput{ + EncoderHiddenStates: req.Embeddings, + HiddenSize: hiddenSize, + } + if len(req.AttentionMask) > 0 { + // Convert []int to []int64 + input.AttentionMask = make([]int64, len(req.AttentionMask)) + for i, v := range req.AttentionMask { + input.AttentionMask[i] = int64(v) + } + } + + ln.logger.Debug("decoding from embeddings", + zap.String("model", req.Model), + zap.Int("seq_len", len(req.Embeddings)), + zap.Int("hidden_size", hiddenSize)) + + output, err = dec.DecodeFromEmbeddings(r.Context(), input, opts) + } else { + // Text prompt path: standard generation + ln.logger.Debug("decoding from prompt", + zap.String("model", req.Model), + zap.String("prompt", req.Prompt)) + + output, err = model.Generate(r.Context(), []string{req.Prompt}) + } + + if err != nil { + ln.logger.Error("decode failed", + zap.String("model", req.Model), + zap.Error(err)) + http.Error(w, fmt.Sprintf("decode failed: %v", err), http.StatusInternalServerError) + return + } + + ln.logger.Info("decode request completed", + zap.String("model", req.Model), + zap.Int("num_outputs", len(output.Texts))) + + // Build response - flatten nested texts + var texts []string + for _, batch := range output.Texts { + texts = append(texts, batch...) + } + + resp := DecodeResponse{ + Model: req.Model, + Texts: texts, + } + + w.Header().Set("Content-Type", "application/json") + if err := encoder.NewStreamEncoder(w).Encode(resp); err != nil { + ln.logger.Error("encoding response", zap.Error(err)) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + diff --git a/pkg/termite/lib/seq2seq/decoder.go b/pkg/termite/lib/seq2seq/decoder.go new file mode 100644 index 0000000..fbb4a21 --- /dev/null +++ b/pkg/termite/lib/seq2seq/decoder.go @@ -0,0 +1,92 @@ +// Copyright 2025 Antfly, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package seq2seq + +import ( + "context" +) + +// DecoderInput represents input for embedding-to-text generation. +// This allows bypassing the encoder and generating text directly from +// pre-computed embeddings (encoder hidden states). +type DecoderInput struct { + // EncoderHiddenStates contains the pre-computed encoder outputs. + // Shape: [sequence_length, hidden_size] for a single input. + // These are passed directly to the decoder as encoder_hidden_states. + EncoderHiddenStates [][]float32 + + // AttentionMask is an optional mask for the encoder hidden states. + // Shape: [sequence_length]. If nil, all positions are attended to. + AttentionMask []int64 + + // HiddenSize is the dimension of the hidden states (for validation). + // This should match the model's hidden_size configuration. + HiddenSize int +} + +// DecodeOptions configures the decoding/generation behavior. +type DecodeOptions struct { + // MaxTokens is the maximum number of tokens to generate. + // Default: 256 + MaxTokens int + + // Temperature controls randomness in sampling. Higher values (e.g., 1.0) + // produce more diverse output, lower values (e.g., 0.1) produce more + // deterministic output. Default: 1.0 + Temperature float32 + + // TopP is the nucleus sampling probability threshold. Tokens are sampled + // from the smallest set of tokens whose cumulative probability exceeds TopP. + // Default: 1.0 (disabled) + TopP float32 + + // TopK limits sampling to the top K most likely tokens. + // Default: 0 (disabled) + TopK int + + // RepetitionPenalty penalizes repeated tokens. Values > 1.0 discourage + // repetition, values < 1.0 encourage it. Default: 1.0 + RepetitionPenalty float32 +} + +// DefaultDecodeOptions returns sensible default options for decoding. +func DefaultDecodeOptions() DecodeOptions { + return DecodeOptions{ + MaxTokens: 256, + Temperature: 1.0, + TopP: 1.0, + TopK: 0, + RepetitionPenalty: 1.2, + } +} + +// Decoder is the interface for models that support generating text from embeddings. +// This enables embedding-to-text generation workflows, including: +// - Embedding inversion (reconstructing text from embeddings) +// - Custom embedding injection (using external/manipulated embeddings) +// - Cross-modal generation (vision embeddings to text) +type Decoder interface { + // DecodeFromEmbeddings generates text from pre-computed encoder hidden states. + // This bypasses the encoder and runs only the decoder, using the provided + // embeddings as encoder_hidden_states for cross-attention. + DecodeFromEmbeddings(ctx context.Context, input *DecoderInput, opts DecodeOptions) (*GeneratedOutput, error) + + // HiddenSize returns the expected embedding dimension for validation. + // Embeddings passed to DecodeFromEmbeddings must have this dimension. + HiddenSize() int + + // Close releases resources held by the decoder. + Close() error +} diff --git a/pkg/termite/lib/seq2seq/t5gemma2.go b/pkg/termite/lib/seq2seq/t5gemma2.go index 6564ae0..a0e622c 100644 --- a/pkg/termite/lib/seq2seq/t5gemma2.go +++ b/pkg/termite/lib/seq2seq/t5gemma2.go @@ -41,8 +41,9 @@ import ( _ "golang.org/x/image/webp" ) -// Ensure T5Gemma2Generator implements the Model interface +// Ensure T5Gemma2Generator implements the Model and Decoder interfaces var _ Model = (*T5Gemma2Generator)(nil) +var _ Decoder = (*T5Gemma2Generator)(nil) // T5Gemma2Generator implements multimodal seq2seq text generation using T5Gemma-2. // It supports text-to-text and image+text-to-text generation. @@ -433,6 +434,80 @@ func (g *T5Gemma2Generator) Config() T5Gemma2GeneratorConfig { return g.config } +// HiddenSize returns the expected embedding dimension for the model. +// This is used to validate embeddings passed to DecodeFromEmbeddings. +func (g *T5Gemma2Generator) HiddenSize() int { + return g.config.HiddenSize +} + +// DecodeFromEmbeddings generates text from pre-computed encoder hidden states. +// This enables embedding-to-text generation workflows where custom embeddings +// (from external sources, manipulated vectors, or vision encoders) are used +// directly for text generation without running the encoder. +// +// The embeddings are used as encoder_hidden_states in the decoder's cross-attention +// mechanism, allowing the decoder to generate text conditioned on these embeddings. +// +// Example use cases: +// - Embedding inversion: Reconstruct text from embeddings +// - Custom embedding injection: Use manipulated or external embeddings +// - Cross-modal generation: Convert vision embeddings to text descriptions +func (g *T5Gemma2Generator) DecodeFromEmbeddings( + ctx context.Context, + input *DecoderInput, + opts DecodeOptions, +) (*GeneratedOutput, error) { + if len(input.EncoderHiddenStates) == 0 { + return nil, errors.New("encoder hidden states are required") + } + + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + // Validate embedding dimensions + expectedDim := g.config.HiddenSize + for i, emb := range input.EncoderHiddenStates { + if len(emb) != expectedDim { + return nil, fmt.Errorf("embedding %d dimension mismatch: got %d, expected %d", + i, len(emb), expectedDim) + } + } + + g.logger.Debug("Starting T5Gemma-2 decode from embeddings", + zap.Int("num_embeddings", len(input.EncoderHiddenStates)), + zap.Int("hidden_size", expectedDim), + zap.Int("max_tokens", opts.MaxTokens)) + + // Apply default options if not specified + if opts.MaxTokens <= 0 { + opts.MaxTokens = g.config.MaxNewTokens + } + if opts.Temperature <= 0 { + opts.Temperature = g.config.Temperature + } + if opts.TopP <= 0 { + opts.TopP = g.config.TopP + } + if opts.RepetitionPenalty <= 0 { + opts.RepetitionPenalty = g.config.RepetitionPenalty + } + + // Run generation using the decoder with custom encoder hidden states + output, err := g.runDecoderWithEmbeddings(ctx, input, opts) + if err != nil { + g.logger.Error("T5Gemma-2 decode from embeddings failed", zap.Error(err)) + return nil, fmt.Errorf("decoding from embeddings: %w", err) + } + + g.logger.Debug("T5Gemma-2 decode from embeddings completed", + zap.Int("num_outputs", len(output.Texts))) + + return output, nil +} + // Close releases resources. func (g *T5Gemma2Generator) Close() error { var errs []error diff --git a/pkg/termite/lib/seq2seq/t5gemma2_decoder.go b/pkg/termite/lib/seq2seq/t5gemma2_decoder.go new file mode 100644 index 0000000..c2c44a7 --- /dev/null +++ b/pkg/termite/lib/seq2seq/t5gemma2_decoder.go @@ -0,0 +1,187 @@ +// Copyright 2025 Antfly, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build onnx && ORT + +package seq2seq + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "github.com/knights-analytics/hugot/backends" + "github.com/knights-analytics/hugot/pipelines" + ort "github.com/yalue/onnxruntime_go" + "go.uber.org/zap" +) + +// runDecoderWithEmbeddings runs the decoder directly with pre-computed encoder hidden states. +// This bypasses the encoder and uses the embeddings as encoder_hidden_states directly +// for the decoder's cross-attention mechanism. +// +// The implementation creates ONNX tensors from the input embeddings and uses hugot's +// generation functions to run the autoregressive decoding loop. +func (g *T5Gemma2Generator) runDecoderWithEmbeddings( + ctx context.Context, + input *DecoderInput, + opts DecodeOptions, +) (*GeneratedOutput, error) { + // Verify decoder ONNX files exist + decoderInitPath := filepath.Join(g.modelPath, "decoder-init.onnx") + decoderPath := filepath.Join(g.modelPath, "decoder.onnx") + + if _, err := os.Stat(decoderInitPath); err != nil { + return nil, fmt.Errorf("decoder-init.onnx not found: %w", err) + } + if _, err := os.Stat(decoderPath); err != nil { + return nil, fmt.Errorf("decoder.onnx not found: %w", err) + } + + // Check context cancellation + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + // Prepare embeddings dimensions + batchSize := 1 // Currently support single-input decoding + seqLen := len(input.EncoderHiddenStates) + hiddenSize := input.HiddenSize + + g.logger.Debug("Running decoder with embeddings", + zap.Int("batch_size", batchSize), + zap.Int("seq_len", seqLen), + zap.Int("hidden_size", hiddenSize), + zap.Int("max_tokens", opts.MaxTokens)) + + // Flatten embeddings to [batch_size * seq_len * hidden_size] + flatEmbeddings := make([]float32, batchSize*seqLen*hiddenSize) + for i, emb := range input.EncoderHiddenStates { + copy(flatEmbeddings[i*hiddenSize:], emb) + } + + // Create encoder_hidden_states tensor [batch_size, seq_len, hidden_size] + encoderHiddenStatesTensor, err := ort.NewTensor( + ort.NewShape(int64(batchSize), int64(seqLen), int64(hiddenSize)), + flatEmbeddings, + ) + if err != nil { + return nil, fmt.Errorf("creating encoder_hidden_states tensor: %w", err) + } + + // Create attention mask + var attentionMask []int64 + if input.AttentionMask != nil && len(input.AttentionMask) > 0 { + attentionMask = input.AttentionMask + } else { + // Default: attend to all positions + attentionMask = make([]int64, seqLen) + for i := range attentionMask { + attentionMask[i] = 1 + } + } + + // Create attention mask tensor [batch_size, seq_len] + flatAttentionMask := make([]int64, batchSize*seqLen) + copy(flatAttentionMask, attentionMask) + + encoderAttentionMaskTensor, err := ort.NewTensor( + ort.NewShape(int64(batchSize), int64(seqLen)), + flatAttentionMask, + ) + if err != nil { + encoderHiddenStatesTensor.Destroy() + return nil, fmt.Errorf("creating encoder_attention_mask tensor: %w", err) + } + + // Create a Seq2SeqBatch and set encoder outputs directly + batch := pipelines.NewSeq2SeqBatch(batchSize) + batch.MaxInputLength = seqLen + batch.SetEncoderHiddenStates(encoderHiddenStatesTensor) + batch.SetEncoderAttentionMask(encoderAttentionMaskTensor) + + // Set cleanup function for encoder tensors + batch.SetDestroyEncoder(func() error { + err1 := encoderHiddenStatesTensor.Destroy() + err2 := encoderAttentionMaskTensor.Destroy() + if err1 != nil { + return err1 + } + return err2 + }) + + // Ensure cleanup on exit + defer func() { + if destroyErr := batch.Destroy(); destroyErr != nil { + g.logger.Warn("Error destroying batch", zap.Error(destroyErr)) + } + }() + + // Run generation using hugot's backend functions + // The seq2seqPipeline implements Seq2SeqPipelineInterface + var genErr error + if opts.Temperature > 0 && opts.Temperature != 1.0 { + // Use sampling with temperature + genErr = backends.RunSeq2SeqGenerationSampling(batch, g.seq2seqPipeline) + } else { + // Use greedy decoding + genErr = backends.RunSeq2SeqGenerationGreedy(batch, g.seq2seqPipeline) + } + + if genErr != nil { + return nil, fmt.Errorf("generation failed: %w", genErr) + } + + // Get generated tokens and decode to text + generatedTokens := batch.GetGeneratedTokens() + + g.logger.Debug("Generation completed", + zap.Int("num_sequences", len(generatedTokens))) + + // Decode tokens to text using the pipeline's tokenizer + texts := make([][]string, len(generatedTokens)) + for i, tokens := range generatedTokens { + // Convert int64 tokens to uint32 for decode function + uint32Tokens := make([]uint32, len(tokens)) + for j, t := range tokens { + uint32Tokens[j] = uint32(t) + } + + // Decode tokens, skipping special tokens + tokenizer := g.seq2seqPipeline.GetTokenizer() + text, decodeErr := backends.Decode(uint32Tokens, tokenizer, true) + if decodeErr != nil { + return nil, fmt.Errorf("decoding tokens: %w", decodeErr) + } + texts[i] = []string{text} + } + + // Convert tokens to uint32 format for output + tokenOutput := make([][][]uint32, len(generatedTokens)) + for i, tokens := range generatedTokens { + uint32Tokens := make([]uint32, len(tokens)) + for j, t := range tokens { + uint32Tokens[j] = uint32(t) + } + tokenOutput[i] = [][]uint32{uint32Tokens} + } + + return &GeneratedOutput{ + Texts: texts, + Tokens: tokenOutput, + }, nil +} diff --git a/pkg/termite/lib/seq2seq/t5gemma2_stub.go b/pkg/termite/lib/seq2seq/t5gemma2_stub.go index 38c8a18..871a18d 100644 --- a/pkg/termite/lib/seq2seq/t5gemma2_stub.go +++ b/pkg/termite/lib/seq2seq/t5gemma2_stub.go @@ -98,6 +98,21 @@ func (g *T5Gemma2Generator) Close() error { return nil } +// HiddenSize returns the expected embedding dimension. +// Stub returns 0 as ONNX is not available. +func (g *T5Gemma2Generator) HiddenSize() int { + return 0 +} + +// DecodeFromEmbeddings is not available without ONNX Runtime. +func (g *T5Gemma2Generator) DecodeFromEmbeddings( + ctx context.Context, + input *DecoderInput, + opts DecodeOptions, +) (*GeneratedOutput, error) { + return nil, errors.New("T5Gemma-2 decode from embeddings not available: build with -tags=\"onnx,ORT\"") +} + // IsT5Gemma2GeneratorModel checks if a model directory contains T5Gemma-2 generator files. func IsT5Gemma2GeneratorModel(modelPath string) bool { // Must have standard seq2seq files diff --git a/pkg/termite/openapi.yaml b/pkg/termite/openapi.yaml index 895948b..42be60d 100644 --- a/pkg/termite/openapi.yaml +++ b/pkg/termite/openapi.yaml @@ -469,6 +469,77 @@ components: type: string description: Rewritten texts (array of arrays, one per input, multiple per beam) + # Decode Types (embedding-to-text generation) + DecodeRequest: + type: object + required: + - model + properties: + model: + type: string + description: Name of T5Gemma-2 or compatible decoder model + example: google/t5gemma-2-270m + prompt: + type: string + description: Input prompt for text generation (mutually exclusive with embeddings) + example: "The meaning of life is" + embeddings: + type: array + items: + type: array + items: + type: number + format: float + description: | + Pre-computed encoder hidden states for embedding-to-text generation. + Shape: [sequence_length, hidden_size] for single input. + When provided, the decoder generates text directly from these embeddings + without running the encoder. Mutually exclusive with 'prompt'. + sequence_length: + type: integer + description: The sequence length of the embeddings (for reshaping flat arrays) + attention_mask: + type: array + items: + type: integer + description: Optional attention mask for embeddings. Shape [sequence_length]. + max_tokens: + type: integer + description: Maximum number of tokens to generate (default 256) + default: 256 + temperature: + type: number + format: float + description: Sampling temperature (default 1.0) + default: 1.0 + top_p: + type: number + format: float + description: Nucleus sampling probability (default 1.0) + default: 1.0 + + DecodeResponse: + type: object + required: + - model + - texts + properties: + model: + type: string + description: Name of model used for decoding + texts: + type: array + items: + type: string + description: Generated text sequences + tokens: + type: array + items: + type: array + items: + type: integer + description: Token IDs for each generated sequence (optional) + # Models Types ModelsResponse: type: object @@ -1565,6 +1636,81 @@ paths: schema: $ref: "#/components/schemas/Error" + /decode: + post: + summary: Decode text from embeddings using T5Gemma-2 + description: | + Generate text using the T5Gemma-2 decoder from pre-computed embeddings or text prompts. + This enables embedding-to-text generation for custom embedding injection workflows. + + ## Models + + - Uses T5Gemma-2 models from `models_dir/rewriters/` that have decoder components + - Requires decoder.onnx and decoder-init.onnx files + + ## Use Cases + + - **Text Continuation**: Generate text continuations from prompts + - **Embedding Inversion**: Reconstruct text from embeddings + - **Guided Generation**: Generate text conditioned on custom encoder outputs + - **Cross-modal Generation**: Generate text from vision embeddings + + ## Example (with prompt) + + ```json + { + "model": "google/t5gemma-2-270m", + "prompt": "The meaning of life is" + } + ``` + + ## Example (with embeddings) + + ```json + { + "model": "google/t5gemma-2-270m", + "embeddings": [[0.1, 0.2, ...], [0.3, 0.4, ...]] + } + ``` + operationId: decodeText + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/DecodeRequest" + responses: + "200": + description: Text decoded successfully + content: + application/json: + schema: + $ref: "#/components/schemas/DecodeResponse" + "400": + description: Invalid request + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "404": + description: Model not found or does not support decoding + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "500": + description: Internal server error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "503": + description: Decode service unavailable (no compatible models) + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /models: get: summary: List available models diff --git a/scripts/export_t5gemma2.py b/scripts/export_t5gemma2.py index f6371e6..dd84819 100755 --- a/scripts/export_t5gemma2.py +++ b/scripts/export_t5gemma2.py @@ -476,6 +476,77 @@ def forward(self, input_ids, attention_mask): logger.info(" Using embedding_layer.onnx for embeddings (without attention)") logger.info(" For full encoder functionality, use PyTorch inference instead of ONNX") + # Export 3: Encoder with inputs_embeds (for embedding-to-text generation) + encoder_embeds_path = output_dir / "encoder_embeds.onnx" + logger.info(" 1c. Exporting encoder with inputs_embeds support...") + + # Get hidden size from model config + if hasattr(model.config, 'encoder'): + hidden_size = model.config.encoder.hidden_size + elif hasattr(model.config, 'hidden_size'): + hidden_size = model.config.hidden_size + else: + hidden_size = 640 # Default for T5Gemma2-270m + + class EncoderEmbedsModule(torch.nn.Module): + """Encoder that accepts pre-computed embeddings directly (inputs_embeds).""" + def __init__(self, encoder): + super().__init__() + self.encoder = encoder + + def forward(self, inputs_embeds, attention_mask): + """Run encoder directly on embeddings, bypassing token embedding lookup.""" + outputs = self.encoder( + inputs_embeds=inputs_embeds, + attention_mask=attention_mask, + output_hidden_states=False, + return_dict=True, + ) + return outputs.last_hidden_state + + encoder_embeds_module = EncoderEmbedsModule(encoder) + encoder_embeds_module.eval() + for param in encoder_embeds_module.parameters(): + param.requires_grad = False + + # Create dummy inputs_embeds + batch_size, seq_len = input_ids.shape + dummy_inputs_embeds = torch.randn(batch_size, seq_len, hidden_size) + + try: + with torch.no_grad(): + torch.onnx.export( + encoder_embeds_module, + (dummy_inputs_embeds, attention_mask), + str(encoder_embeds_path), + export_params=True, + opset_version=OPSET_VERSION, + do_constant_folding=True, + input_names=["inputs_embeds", "attention_mask"], + output_names=["encoder_hidden_states"], + dynamic_axes={ + "inputs_embeds": {0: "batch_size", 1: "sequence_length"}, + "attention_mask": {0: "batch_size", 1: "sequence_length"}, + "encoder_hidden_states": {0: "batch_size", 1: "sequence_length"}, + }, + dynamo=False, + ) + + # Validate + onnx_model = onnx.load(str(encoder_embeds_path)) + onnx.checker.check_model(onnx_model) + size_mb = encoder_embeds_path.stat().st_size / (1024 * 1024) + logger.info(f" Saved: encoder_embeds.onnx ({size_mb:.1f} MB)") + + # Test encoder_embeds output shape + with torch.no_grad(): + test_output = encoder_embeds_module(dummy_inputs_embeds, attention_mask) + logger.info(f" Encoder (embeds) output shape: {test_output.shape}") + + except Exception as e: + logger.warning(f" Encoder with inputs_embeds export failed: {e}") + logger.info(" Embedding-to-text generation will require the standard encoder path") + def export_vision_encoder(model, processor, output_dir: Path) -> bool: """Export the vision encoder (SigLIP) to ONNX if present.""" From 549bb97f5f555386367c5aff623cb462417956ae Mon Sep 17 00:00:00 2001 From: timkaye11 Date: Sun, 4 Jan 2026 10:31:28 -0800 Subject: [PATCH 4/7] hidden states wip --- openapi.yaml | 32 +- pkg/client/oapi/client.gen.go | 420 ++++++++++---------- pkg/termite/api.gen.go | 420 ++++++++++---------- pkg/termite/api.go | 50 ++- pkg/termite/lib/embeddings/hidden_states.go | 50 +++ pkg/termite/lib/embeddings/t5gemma2.go | 112 ++++++ pkg/termite/lib/seq2seq/t5gemma2.go | 11 +- pkg/termite/openapi.yaml | 32 +- 8 files changed, 713 insertions(+), 414 deletions(-) create mode 100644 pkg/termite/lib/embeddings/hidden_states.go diff --git a/openapi.yaml b/openapi.yaml index 621c519..6304b42 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -753,6 +753,12 @@ components: type: boolean default: true description: Truncate input to fit model context length + return_hidden_states: + type: boolean + default: false + description: | + If true, returns raw encoder hidden states [seq_len, hidden_size] per input + instead of mean-pooled embeddings. Useful for passing to the decode API. EmbedResponse: type: object example: @@ -766,7 +772,6 @@ components: - 0.089 required: - model - - embeddings properties: model: type: string @@ -779,7 +784,30 @@ components: items: type: number format: float - description: Array of embedding vectors (one per input string) + description: | + Array of mean-pooled embedding vectors [hidden_size] per input. + Present when return_hidden_states is false (default). + hidden_states: + type: array + items: + type: array + items: + type: array + items: + type: number + format: float + description: | + Raw encoder hidden states [seq_len, hidden_size] per input. + Present when return_hidden_states is true. + attention_mask: + type: array + items: + type: array + items: + type: integer + description: | + Attention mask [seq_len] per input indicating valid tokens. + Present when return_hidden_states is true. Chunk: type: object description: A chunk of text with position information. diff --git a/pkg/client/oapi/client.gen.go b/pkg/client/oapi/client.gen.go index 84d1604..c76dcc8 100644 --- a/pkg/client/oapi/client.gen.go +++ b/pkg/client/oapi/client.gen.go @@ -340,6 +340,10 @@ type EmbedRequest struct { // Model Name of the embedder model from models_dir/embedders/ Model string `json:"model"` + // ReturnHiddenStates If true, returns raw encoder hidden states [seq_len, hidden_size] per input + // instead of mean-pooled embeddings. Useful for passing to the decode API. + ReturnHiddenStates bool `json:"return_hidden_states,omitempty,omitzero"` + // Truncate Truncate input to fit model context length Truncate bool `json:"truncate,omitempty,omitzero"` } @@ -363,8 +367,17 @@ type EmbedRequest_Input struct { // EmbedResponse defines model for EmbedResponse. type EmbedResponse struct { - // Embeddings Array of embedding vectors (one per input string) - Embeddings [][]float32 `json:"embeddings"` + // AttentionMask Attention mask [seq_len] per input indicating valid tokens. + // Present when return_hidden_states is true. + AttentionMask [][]int `json:"attention_mask,omitempty,omitzero"` + + // Embeddings Array of mean-pooled embedding vectors [hidden_size] per input. + // Present when return_hidden_states is false (default). + Embeddings [][]float32 `json:"embeddings,omitempty,omitzero"` + + // HiddenStates Raw encoder hidden states [seq_len, hidden_size] per input. + // Present when return_hidden_states is true. + HiddenStates [][][]float32 `json:"hidden_states,omitempty,omitzero"` // Model Model used for embedding Model string `json:"model"` @@ -2621,206 +2634,209 @@ func ParseGetVersionResponse(rsp *http.Response) (*GetVersionResponse, error) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/pkg/termite/api.gen.go b/pkg/termite/api.gen.go index 95eae57..46b5765 100644 --- a/pkg/termite/api.gen.go +++ b/pkg/termite/api.gen.go @@ -340,6 +340,10 @@ type EmbedRequest struct { // Model Name of the embedder model from models_dir/embedders/ Model string `json:"model"` + // ReturnHiddenStates If true, returns raw encoder hidden states [seq_len, hidden_size] per input + // instead of mean-pooled embeddings. Useful for passing to the decode API. + ReturnHiddenStates bool `json:"return_hidden_states,omitempty,omitzero"` + // Truncate Truncate input to fit model context length Truncate bool `json:"truncate,omitempty,omitzero"` } @@ -363,8 +367,17 @@ type EmbedRequest_Input struct { // EmbedResponse defines model for EmbedResponse. type EmbedResponse struct { - // Embeddings Array of embedding vectors (one per input string) - Embeddings [][]float32 `json:"embeddings"` + // AttentionMask Attention mask [seq_len] per input indicating valid tokens. + // Present when return_hidden_states is true. + AttentionMask [][]int `json:"attention_mask,omitempty,omitzero"` + + // Embeddings Array of mean-pooled embedding vectors [hidden_size] per input. + // Present when return_hidden_states is false (default). + Embeddings [][]float32 `json:"embeddings,omitempty,omitzero"` + + // HiddenStates Raw encoder hidden states [seq_len, hidden_size] per input. + // Present when return_hidden_states is true. + HiddenStates [][][]float32 `json:"hidden_states,omitempty,omitzero"` // Model Model used for embedding Model string `json:"model"` @@ -1395,206 +1408,209 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/pkg/termite/api.go b/pkg/termite/api.go index 6480b82..f5432e4 100644 --- a/pkg/termite/api.go +++ b/pkg/termite/api.go @@ -29,9 +29,10 @@ import ( "time" "github.com/antflydb/antfly-go/libaf/ai" - "github.com/antflydb/antfly-go/libaf/embeddings" + libafembed "github.com/antflydb/antfly-go/libaf/embeddings" "github.com/antflydb/antfly-go/libaf/s3" "github.com/antflydb/antfly-go/libaf/scraping" + "github.com/antflydb/termite/pkg/termite/lib/embeddings" "github.com/antflydb/termite/pkg/termite/lib/generation" "github.com/antflydb/termite/pkg/termite/lib/ner" "github.com/antflydb/termite/pkg/termite/lib/seq2seq" @@ -243,6 +244,49 @@ func (ln *TermiteNode) handleApiEmbed(w http.ResponseWriter, r *http.Request) { return } + // Check if hidden states are requested + if req.ReturnHiddenStates { + // Check if embedder supports hidden states extraction + hsEmbedder, ok := embedder.(embeddings.HiddenStatesEmbedder) + if !ok { + http.Error(w, fmt.Sprintf("model %s does not support hidden states extraction", req.Model), http.StatusBadRequest) + return + } + + // Generate hidden states (skip caching - different output format) + output, err := hsEmbedder.EmbedWithHiddenStates(r.Context(), contents) + if err != nil { + ln.logger.Error("failed to extract hidden states", + zap.String("model", req.Model), + zap.Error(err)) + http.Error(w, fmt.Sprintf("extracting hidden states: %v", err), http.StatusInternalServerError) + return + } + + // Convert int64 attention mask to int for JSON serialization + attentionMask := make([][]int, len(output.AttentionMask)) + for i, mask := range output.AttentionMask { + attentionMask[i] = make([]int, len(mask)) + for j, v := range mask { + attentionMask[i][j] = int(v) + } + } + + // Return hidden states response (always JSON for now) + resp := EmbedResponse{ + Model: req.Model, + HiddenStates: output.HiddenStates, + AttentionMask: attentionMask, + } + w.Header().Set("Content-Type", "application/json") + if err := encoder.NewStreamEncoder(w).Encode(resp); err != nil { + ln.logger.Error("encoding hidden states response", zap.Error(err)) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + return + } + // Wrap embedder with caching for deduplicated requests cachedEmbedder := ln.embeddingCache.WrapEmbedder(embedder, req.Model) @@ -348,7 +392,7 @@ func parseEmbedInput( // validateContentTypes checks that all content types in the input are supported // by the embedder's capabilities. -func validateContentTypes(contents [][]ai.ContentPart, caps embeddings.EmbedderCapabilities) error { +func validateContentTypes(contents [][]ai.ContentPart, caps libafembed.EmbedderCapabilities) error { // Build set of supported MIME types supported := make(map[string]bool) for _, m := range caps.SupportedMIMETypes { @@ -377,7 +421,7 @@ func validateContentTypes(contents [][]ai.ContentPart, caps embeddings.EmbedderC } // getMIMETypeList returns a list of supported MIME types for error messages. -func getMIMETypeList(caps embeddings.EmbedderCapabilities) []string { +func getMIMETypeList(caps libafembed.EmbedderCapabilities) []string { types := make([]string, len(caps.SupportedMIMETypes)) for i, m := range caps.SupportedMIMETypes { types[i] = m.MIMEType diff --git a/pkg/termite/lib/embeddings/hidden_states.go b/pkg/termite/lib/embeddings/hidden_states.go new file mode 100644 index 0000000..c64464f --- /dev/null +++ b/pkg/termite/lib/embeddings/hidden_states.go @@ -0,0 +1,50 @@ +// Copyright 2025 Antfly, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package embeddings + +import ( + "context" + + "github.com/antflydb/antfly-go/libaf/ai" + "github.com/antflydb/antfly-go/libaf/embeddings" +) + +// HiddenStatesOutput contains raw encoder hidden states (before pooling). +type HiddenStatesOutput struct { + // HiddenStates are raw encoder outputs [batch_size][seq_len][hidden_size]. + // Each input produces a sequence of hidden state vectors, one per token. + HiddenStates [][][]float32 + + // AttentionMask indicates valid token positions [batch_size][seq_len]. + // 1 = valid token, 0 = padding token. + AttentionMask [][]int64 +} + +// HiddenStatesEmbedder extends Embedder with raw hidden state extraction. +// This is useful for: +// - Passing hidden states to the decode API for text generation +// - Custom pooling strategies (e.g., first token, last token, weighted) +// - Token-level similarity comparisons +type HiddenStatesEmbedder interface { + embeddings.Embedder + + // EmbedWithHiddenStates returns raw encoder hidden states before pooling. + // Unlike Embed() which returns mean-pooled vectors, this returns the full + // sequence of token-level hidden states. + // + // The returned hidden states can be passed to the decode API's embeddings + // field for text generation from custom embeddings. + EmbedWithHiddenStates(ctx context.Context, contents [][]ai.ContentPart) (*HiddenStatesOutput, error) +} diff --git a/pkg/termite/lib/embeddings/t5gemma2.go b/pkg/termite/lib/embeddings/t5gemma2.go index ba0de42..df62c00 100644 --- a/pkg/termite/lib/embeddings/t5gemma2.go +++ b/pkg/termite/lib/embeddings/t5gemma2.go @@ -463,6 +463,118 @@ func loadT5Gemma2Config(modelPath string) (*T5Gemma2Config, error) { }, nil } +// Ensure T5Gemma2Embedder implements HiddenStatesEmbedder +var _ HiddenStatesEmbedder = (*T5Gemma2Embedder)(nil) + +// EmbedWithHiddenStates returns raw encoder hidden states before mean pooling. +// This is useful for passing to the decode API for text generation from custom embeddings. +// +// The returned hidden states have shape [batch_size][seq_len][hidden_size] where: +// - batch_size: number of input texts +// - seq_len: number of tokens in each input (varies per input) +// - hidden_size: 640 for T5Gemma-2 270M +func (t *T5Gemma2Embedder) EmbedWithHiddenStates(ctx context.Context, contents [][]ai.ContentPart) (*HiddenStatesOutput, error) { + if len(contents) == 0 { + return &HiddenStatesOutput{ + HiddenStates: [][][]float32{}, + AttentionMask: [][]int64{}, + }, nil + } + + // Extract text from contents (only text supported for hidden states currently) + texts := make([]string, 0, len(contents)) + for _, parts := range contents { + for _, part := range parts { + if tc, ok := part.(ai.TextContent); ok { + texts = append(texts, tc.Text) + break + } + } + } + + if len(texts) == 0 { + return nil, errors.New("no text content found in inputs") + } + + // Create batch and run preprocessing (tokenization) + batch := backends.NewBatch(len(texts)) + defer func() { + if batch.DestroyInputs != nil { + _ = batch.DestroyInputs() + } + }() + + if err := t.encoderPipeline.Preprocess(batch, texts); err != nil { + return nil, fmt.Errorf("preprocessing inputs: %w", err) + } + + // Run forward pass (encoder inference) + if err := t.encoderPipeline.Forward(batch); err != nil { + return nil, fmt.Errorf("running encoder forward: %w", err) + } + + // Extract raw hidden states from batch.OutputValues + // The output shape is [batch_size, seq_len, hidden_size] + if len(batch.OutputValues) == 0 { + return nil, errors.New("no output from encoder") + } + + hiddenStates, attentionMask, err := t.extractHiddenStates(batch) + if err != nil { + return nil, fmt.Errorf("extracting hidden states: %w", err) + } + + return &HiddenStatesOutput{ + HiddenStates: hiddenStates, + AttentionMask: attentionMask, + }, nil +} + +// extractHiddenStates converts batch.OutputValues to the expected format. +// The raw ONNX output is typically [batch, seq_len, hidden_size] as float32. +func (t *T5Gemma2Embedder) extractHiddenStates(batch *backends.PipelineBatch) ([][][]float32, [][]int64, error) { + // Get the first output (encoder hidden states) + rawOutput := batch.OutputValues[0] + + // The output can be in different formats depending on the backend + var hiddenStates [][][]float32 + + switch v := rawOutput.(type) { + case [][][]float32: + // Already in the expected format [batch, seq_len, hidden_size] + hiddenStates = v + case [][]float32: + // Flattened format [batch, seq_len * hidden_size] - need to reshape + hiddenSize := t.config.HiddenSize + hiddenStates = make([][][]float32, len(v)) + for i, flat := range v { + seqLen := len(flat) / hiddenSize + if seqLen*hiddenSize != len(flat) { + return nil, nil, fmt.Errorf("unexpected embedding dimension: %d (not divisible by hidden_size %d)", len(flat), hiddenSize) + } + hiddenStates[i] = make([][]float32, seqLen) + for j := 0; j < seqLen; j++ { + hiddenStates[i][j] = flat[j*hiddenSize : (j+1)*hiddenSize] + } + } + default: + return nil, nil, fmt.Errorf("unexpected output type: %T", rawOutput) + } + + // Extract attention masks from batch.Input, trimmed to match hidden states length + attentionMask := make([][]int64, len(hiddenStates)) + for i := range hiddenStates { + seqLen := len(hiddenStates[i]) + attentionMask[i] = make([]int64, seqLen) + // Fill with 1s for actual tokens (hidden states only contain non-padded tokens) + for j := 0; j < seqLen; j++ { + attentionMask[i][j] = 1 + } + } + + return hiddenStates, attentionMask, nil +} + // IsT5Gemma2Model checks if a model directory contains T5Gemma-2 model files. // Only encoder.onnx is required; vision_encoder.onnx is optional for text-only use. func IsT5Gemma2Model(modelPath string) bool { diff --git a/pkg/termite/lib/seq2seq/t5gemma2.go b/pkg/termite/lib/seq2seq/t5gemma2.go index a0e622c..1632b8d 100644 --- a/pkg/termite/lib/seq2seq/t5gemma2.go +++ b/pkg/termite/lib/seq2seq/t5gemma2.go @@ -60,6 +60,7 @@ type T5Gemma2Generator struct { sessionShared bool config T5Gemma2GeneratorConfig modelPath string + pipelineName string // pipeline name for hugot registry cleanup // Lazy loading for vision encoder hasVisionFile bool // true if vision_encoder.onnx exists @@ -213,6 +214,7 @@ func NewT5Gemma2GeneratorWithSession(modelPath string, sharedSession *khugot.Ses sessionShared: sessionShared, config: config, modelPath: modelPath, + pipelineName: pipelineName, hasVisionFile: hasVision, }, nil } @@ -512,9 +514,12 @@ func (g *T5Gemma2Generator) DecodeFromEmbeddings( func (g *T5Gemma2Generator) Close() error { var errs []error - if g.seq2seqPipeline != nil { - if err := g.seq2seqPipeline.Destroy(); err != nil { - errs = append(errs, fmt.Errorf("destroying seq2seq pipeline: %w", err)) + // Use ClosePipeline to properly remove from session registry AND destroy resources. + // Just calling Destroy() would leave a stale entry in the registry, causing + // "pipeline already initialised" errors on subsequent loads. + if g.seq2seqPipeline != nil && g.session != nil && g.pipelineName != "" { + if err := khugot.ClosePipeline[*pipelines.Seq2SeqPipeline](g.session, g.pipelineName); err != nil { + errs = append(errs, fmt.Errorf("closing seq2seq pipeline: %w", err)) } } diff --git a/pkg/termite/openapi.yaml b/pkg/termite/openapi.yaml index 42be60d..d11acf0 100644 --- a/pkg/termite/openapi.yaml +++ b/pkg/termite/openapi.yaml @@ -151,6 +151,12 @@ components: type: boolean default: true description: Truncate input to fit model context length + return_hidden_states: + type: boolean + default: false + description: | + If true, returns raw encoder hidden states [seq_len, hidden_size] per input + instead of mean-pooled embeddings. Useful for passing to the decode API. EmbedResponse: type: object @@ -161,7 +167,6 @@ components: } required: - model - - embeddings properties: model: type: string @@ -174,7 +179,30 @@ components: items: type: number format: float - description: Array of embedding vectors (one per input string) + description: | + Array of mean-pooled embedding vectors [hidden_size] per input. + Present when return_hidden_states is false (default). + hidden_states: + type: array + items: + type: array + items: + type: array + items: + type: number + format: float + description: | + Raw encoder hidden states [seq_len, hidden_size] per input. + Present when return_hidden_states is true. + attention_mask: + type: array + items: + type: array + items: + type: integer + description: | + Attention mask [seq_len] per input indicating valid tokens. + Present when return_hidden_states is true. # Chunking Types - reference existing schemas Chunk: From 79922035d5651a3adf8046beedec9632bc84fd43 Mon Sep 17 00:00:00 2001 From: timkaye11 Date: Mon, 5 Jan 2026 22:05:34 -0800 Subject: [PATCH 5/7] cleanup --- pkg/client/oapi/client.gen.go | 628 ++++++++++++++------ pkg/termite/api.gen.go | 475 +++++++++------ pkg/termite/lib/seq2seq/t5gemma2_decoder.go | 18 +- 3 files changed, 733 insertions(+), 388 deletions(-) diff --git a/pkg/client/oapi/client.gen.go b/pkg/client/oapi/client.gen.go index b09a85b..36dd4e7 100644 --- a/pkg/client/oapi/client.gen.go +++ b/pkg/client/oapi/client.gen.go @@ -292,6 +292,48 @@ type ContentPart struct { union json.RawMessage } +// DecodeRequest defines model for DecodeRequest. +type DecodeRequest struct { + // AttentionMask Optional attention mask for embeddings. Shape [sequence_length]. + AttentionMask []int `json:"attention_mask,omitempty,omitzero"` + + // Embeddings Pre-computed encoder hidden states for embedding-to-text generation. + // Shape: [sequence_length, hidden_size] for single input. + // When provided, the decoder generates text directly from these embeddings + // without running the encoder. Mutually exclusive with 'prompt'. + Embeddings [][]float32 `json:"embeddings,omitempty,omitzero"` + + // MaxTokens Maximum number of tokens to generate (default 256) + MaxTokens int `json:"max_tokens,omitempty,omitzero"` + + // Model Name of T5Gemma-2 or compatible decoder model + Model string `json:"model"` + + // Prompt Input prompt for text generation (mutually exclusive with embeddings) + Prompt string `json:"prompt,omitempty,omitzero"` + + // SequenceLength The sequence length of the embeddings (for reshaping flat arrays) + SequenceLength int `json:"sequence_length,omitempty,omitzero"` + + // Temperature Sampling temperature (default 1.0) + Temperature float32 `json:"temperature,omitempty,omitzero"` + + // TopP Nucleus sampling probability (default 1.0) + TopP float32 `json:"top_p,omitempty,omitzero"` +} + +// DecodeResponse defines model for DecodeResponse. +type DecodeResponse struct { + // Model Name of model used for decoding + Model string `json:"model"` + + // Texts Generated text sequences + Texts []string `json:"texts"` + + // Tokens Token IDs for each generated sequence (optional) + Tokens [][]int `json:"tokens,omitempty,omitzero"` +} + // EmbedRequest defines model for EmbedRequest. type EmbedRequest struct { // Input Input content to embed. Supports three formats: @@ -303,6 +345,10 @@ type EmbedRequest struct { // Model Name of the embedder model from models_dir/embedders/ Model string `json:"model"` + // ReturnHiddenStates If true, returns raw encoder hidden states [seq_len, hidden_size] per input + // instead of mean-pooled embeddings. Useful for passing to the decode API. + ReturnHiddenStates bool `json:"return_hidden_states,omitempty,omitzero"` + // Truncate Truncate input to fit model context length Truncate bool `json:"truncate,omitempty,omitzero"` } @@ -326,8 +372,17 @@ type EmbedRequest_Input struct { // EmbedResponse defines model for EmbedResponse. type EmbedResponse struct { - // Embeddings Array of embedding vectors (one per input string) - Embeddings [][]float32 `json:"embeddings"` + // AttentionMask Attention mask [seq_len] per input indicating valid tokens. + // Present when return_hidden_states is true. + AttentionMask [][]int `json:"attention_mask,omitempty,omitzero"` + + // Embeddings Array of mean-pooled embedding vectors [hidden_size] per input. + // Present when return_hidden_states is false (default). + Embeddings [][]float32 `json:"embeddings,omitempty,omitzero"` + + // HiddenStates Raw encoder hidden states [seq_len, hidden_size] per input. + // Present when return_hidden_states is true. + HiddenStates [][][]float32 `json:"hidden_states,omitempty,omitzero"` // Model Model used for embedding Model string `json:"model"` @@ -764,6 +819,9 @@ type VersionResponse struct { // ChunkTextJSONRequestBody defines body for ChunkText for application/json ContentType. type ChunkTextJSONRequestBody = ChunkRequest +// DecodeTextJSONRequestBody defines body for DecodeText for application/json ContentType. +type DecodeTextJSONRequestBody = DecodeRequest + // GenerateEmbeddingsJSONRequestBody defines body for GenerateEmbeddings for application/json ContentType. type GenerateEmbeddingsJSONRequestBody = EmbedRequest @@ -1131,6 +1189,11 @@ type ClientInterface interface { ChunkText(ctx context.Context, body ChunkTextJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // DecodeTextWithBody request with any body + DecodeTextWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + DecodeText(ctx context.Context, body DecodeTextJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // GenerateEmbeddingsWithBody request with any body GenerateEmbeddingsWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1187,6 +1250,30 @@ func (c *Client) ChunkText(ctx context.Context, body ChunkTextJSONRequestBody, r return c.Client.Do(req) } +func (c *Client) DecodeTextWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDecodeTextRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) DecodeText(ctx context.Context, body DecodeTextJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDecodeTextRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) GenerateEmbeddingsWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewGenerateEmbeddingsRequestWithBody(c.Server, contentType, body) if err != nil { @@ -1371,6 +1458,46 @@ func NewChunkTextRequestWithBody(server string, contentType string, body io.Read return req, nil } +// NewDecodeTextRequest calls the generic DecodeText builder with application/json body +func NewDecodeTextRequest(server string, body DecodeTextJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewDecodeTextRequestWithBody(server, "application/json", bodyReader) +} + +// NewDecodeTextRequestWithBody generates requests for DecodeText with any type of body +func NewDecodeTextRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/decode") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + // NewGenerateEmbeddingsRequest calls the generic GenerateEmbeddings builder with application/json body func NewGenerateEmbeddingsRequest(server string, body GenerateEmbeddingsJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -1673,6 +1800,11 @@ type ClientWithResponsesInterface interface { ChunkTextWithResponse(ctx context.Context, body ChunkTextJSONRequestBody, reqEditors ...RequestEditorFn) (*ChunkTextResponse, error) + // DecodeTextWithBodyWithResponse request with any body + DecodeTextWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*DecodeTextResponse, error) + + DecodeTextWithResponse(ctx context.Context, body DecodeTextJSONRequestBody, reqEditors ...RequestEditorFn) (*DecodeTextResponse, error) + // GenerateEmbeddingsWithBodyWithResponse request with any body GenerateEmbeddingsWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*GenerateEmbeddingsResponse, error) @@ -1729,6 +1861,32 @@ func (r ChunkTextResponse) StatusCode() int { return 0 } +type DecodeTextResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *DecodeResponse + JSON400 *Error + JSON404 *Error + JSON500 *Error + JSON503 *Error +} + +// Status returns HTTPResponse.Status +func (r DecodeTextResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r DecodeTextResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type GenerateEmbeddingsResponse struct { Body []byte HTTPResponse *http.Response @@ -1922,6 +2080,23 @@ func (c *ClientWithResponses) ChunkTextWithResponse(ctx context.Context, body Ch return ParseChunkTextResponse(rsp) } +// DecodeTextWithBodyWithResponse request with arbitrary body returning *DecodeTextResponse +func (c *ClientWithResponses) DecodeTextWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*DecodeTextResponse, error) { + rsp, err := c.DecodeTextWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseDecodeTextResponse(rsp) +} + +func (c *ClientWithResponses) DecodeTextWithResponse(ctx context.Context, body DecodeTextJSONRequestBody, reqEditors ...RequestEditorFn) (*DecodeTextResponse, error) { + rsp, err := c.DecodeText(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseDecodeTextResponse(rsp) +} + // GenerateEmbeddingsWithBodyWithResponse request with arbitrary body returning *GenerateEmbeddingsResponse func (c *ClientWithResponses) GenerateEmbeddingsWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*GenerateEmbeddingsResponse, error) { rsp, err := c.GenerateEmbeddingsWithBody(ctx, contentType, body, reqEditors...) @@ -2065,6 +2240,60 @@ func ParseChunkTextResponse(rsp *http.Response) (*ChunkTextResponse, error) { return response, nil } +// ParseDecodeTextResponse parses an HTTP response from a DecodeTextWithResponse call +func ParseDecodeTextResponse(rsp *http.Response) (*DecodeTextResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &DecodeTextResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest DecodeResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 503: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON503 = &dest + + } + + return response, nil +} + // ParseGenerateEmbeddingsResponse parses an HTTP response from a GenerateEmbeddingsWithResponse call func ParseGenerateEmbeddingsResponse(rsp *http.Response) (*GenerateEmbeddingsResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -2410,197 +2639,210 @@ func ParseGetVersionResponse(rsp *http.Response) (*GetVersionResponse, error) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/pkg/termite/api.gen.go b/pkg/termite/api.gen.go index 12561ee..a93340d 100644 --- a/pkg/termite/api.gen.go +++ b/pkg/termite/api.gen.go @@ -292,6 +292,48 @@ type ContentPart struct { union json.RawMessage } +// DecodeRequest defines model for DecodeRequest. +type DecodeRequest struct { + // AttentionMask Optional attention mask for embeddings. Shape [sequence_length]. + AttentionMask []int `json:"attention_mask,omitempty,omitzero"` + + // Embeddings Pre-computed encoder hidden states for embedding-to-text generation. + // Shape: [sequence_length, hidden_size] for single input. + // When provided, the decoder generates text directly from these embeddings + // without running the encoder. Mutually exclusive with 'prompt'. + Embeddings [][]float32 `json:"embeddings,omitempty,omitzero"` + + // MaxTokens Maximum number of tokens to generate (default 256) + MaxTokens int `json:"max_tokens,omitempty,omitzero"` + + // Model Name of T5Gemma-2 or compatible decoder model + Model string `json:"model"` + + // Prompt Input prompt for text generation (mutually exclusive with embeddings) + Prompt string `json:"prompt,omitempty,omitzero"` + + // SequenceLength The sequence length of the embeddings (for reshaping flat arrays) + SequenceLength int `json:"sequence_length,omitempty,omitzero"` + + // Temperature Sampling temperature (default 1.0) + Temperature float32 `json:"temperature,omitempty,omitzero"` + + // TopP Nucleus sampling probability (default 1.0) + TopP float32 `json:"top_p,omitempty,omitzero"` +} + +// DecodeResponse defines model for DecodeResponse. +type DecodeResponse struct { + // Model Name of model used for decoding + Model string `json:"model"` + + // Texts Generated text sequences + Texts []string `json:"texts"` + + // Tokens Token IDs for each generated sequence (optional) + Tokens [][]int `json:"tokens,omitempty,omitzero"` +} + // EmbedRequest defines model for EmbedRequest. type EmbedRequest struct { // Input Input content to embed. Supports three formats: @@ -303,6 +345,10 @@ type EmbedRequest struct { // Model Name of the embedder model from models_dir/embedders/ Model string `json:"model"` + // ReturnHiddenStates If true, returns raw encoder hidden states [seq_len, hidden_size] per input + // instead of mean-pooled embeddings. Useful for passing to the decode API. + ReturnHiddenStates bool `json:"return_hidden_states,omitempty,omitzero"` + // Truncate Truncate input to fit model context length Truncate bool `json:"truncate,omitempty,omitzero"` } @@ -326,8 +372,17 @@ type EmbedRequest_Input struct { // EmbedResponse defines model for EmbedResponse. type EmbedResponse struct { - // Embeddings Array of embedding vectors (one per input string) - Embeddings [][]float32 `json:"embeddings"` + // AttentionMask Attention mask [seq_len] per input indicating valid tokens. + // Present when return_hidden_states is true. + AttentionMask [][]int `json:"attention_mask,omitempty,omitzero"` + + // Embeddings Array of mean-pooled embedding vectors [hidden_size] per input. + // Present when return_hidden_states is false (default). + Embeddings [][]float32 `json:"embeddings,omitempty,omitzero"` + + // HiddenStates Raw encoder hidden states [seq_len, hidden_size] per input. + // Present when return_hidden_states is true. + HiddenStates [][][]float32 `json:"hidden_states,omitempty,omitzero"` // Model Model used for embedding Model string `json:"model"` @@ -764,6 +819,9 @@ type VersionResponse struct { // ChunkTextJSONRequestBody defines body for ChunkText for application/json ContentType. type ChunkTextJSONRequestBody = ChunkRequest +// DecodeTextJSONRequestBody defines body for DecodeText for application/json ContentType. +type DecodeTextJSONRequestBody = DecodeRequest + // GenerateEmbeddingsJSONRequestBody defines body for GenerateEmbeddings for application/json ContentType. type GenerateEmbeddingsJSONRequestBody = EmbedRequest @@ -1058,6 +1116,9 @@ type ServerInterface interface { // Chunk text into smaller segments // (POST /chunk) ChunkText(w http.ResponseWriter, r *http.Request) + // Decode text from embeddings using T5Gemma-2 + // (POST /decode) + DecodeText(w http.ResponseWriter, r *http.Request) // Generate embeddings // (POST /embed) GenerateEmbeddings(w http.ResponseWriter, r *http.Request) @@ -1104,6 +1165,20 @@ func (siw *ServerInterfaceWrapper) ChunkText(w http.ResponseWriter, r *http.Requ handler.ServeHTTP(w, r) } +// DecodeText operation middleware +func (siw *ServerInterfaceWrapper) DecodeText(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.DecodeText(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // GenerateEmbeddings operation middleware func (siw *ServerInterfaceWrapper) GenerateEmbeddings(w http.ResponseWriter, r *http.Request) { @@ -1323,6 +1398,7 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H } m.HandleFunc("POST "+options.BaseURL+"/chunk", wrapper.ChunkText) + m.HandleFunc("POST "+options.BaseURL+"/decode", wrapper.DecodeText) m.HandleFunc("POST "+options.BaseURL+"/embed", wrapper.GenerateEmbeddings) m.HandleFunc("POST "+options.BaseURL+"/generate", wrapper.GenerateContent) m.HandleFunc("GET "+options.BaseURL+"/models", wrapper.ListModels) @@ -1337,197 +1413,210 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+y9C28ct5Io/Ff4dT7AUrbnIclOnLkIFrIs+2hXtnUk+SR7PYbE6ebM8KiHbDfZkiaB", - "7m+/YBXJZr9GoyTOHtwNECDWNFkki8V6sar4a5TIVS4FE1pFk18jlSzZisI/j5ZUv2NK0QUzf+aFzFmh", - "OYOPiRSaCW3++f8XbB5Nom9GFaSRBTMKYBzZHg9xVMiMPdbz3LR5iCMtZXaV0Cy74qnpkzKVFDzXXIpo", - "Ep28JnJO9JIR046YdkQvuSIrHJRwRQqmcilSLhZES7IjRbYmc1kQM4sfTbfdKI70OmfRJFK64GJRG1e1", - "R710YymyoikjszVMgSrFlaZCNwfxH8xIXLOVemz1ZoQjmmUwE5waLQq6jh4M+tiXkhcsjSafEJWffSM5", - "+ydLAMcdmG+tw34ndjOH5KLMc1loRfSdNNNfUa0mUzEgF3yVZ4wgeiZkGv2NZZmMyVLeEVowspblv08j", - "0/LQTNNsigVKcmoA7nzImTg8Iasy03wlU5pZ+LsT8unXKcx/GhnImt3raRT7f/nRptHD56mI4kgK9mEe", - "TT41l2Mnabq50Tt2ttnryRPedg8t2s9oodvb+Bm2qBQ37V05JIn5AIRtlnLH9ZLkUnHznXCB0+BSDKO4", - "cSqZSK+SJS3aQI+WtKCJZkUIiciCL7igmR1oyQpmB2ciVWSH3SdZqfgtC44IF5otWGGW1HUiL9iXkomE", - "EVGuZqxwxxOh7oxjsheT/ZgMh8MOmHF0P1jIgf215EIf7JuBlKaF/oNWBrBU53pM247T7qffS1SNQ8nT", - "yAKrTT2u9qf7xJbi5kiKOV90rBJ+LwvYeOAsMCXD1MzITJlDK8klK1ZcM3J4djKcikvDCbkilChzNPic", - "s9QsYs4XAMJszN8uL89MczIgKZ/PWaHIvJAr+DYvs4zAtFiBE5iKuyVPloSLJCtTpkheyFuesoIolrEE", - "JkdFShKaLM3cknDaQzi9dYpd0fsrWIlls3NaZjqavBjHTV5F7/mqXAVkhd3Mqgumy8LAZvfUsADs397f", - "lUxZVhsnmvN7ZnarZ8vNGqCXGaZUbEiOuV6ygjyDjs8AjcryHXnDxGBGlUGy7RwTWRBqQQi6Yohc+FuN", - "EkStGv1qPj2MhuES/NRaoknesiKj+RUM+Bje3nt82W45CkLTlcyYvmNMWFQ+jkDFclpQLYs6EqcCdraB", - "Q3PwfAdAFKzI46a2WAuiLYZpsWC6e6mttV5C45Dz4DJzZo9LfYWdS9TLgqmlzNLaYOPhi7jrRKbA6nwf", - "WOWH9+9/tjtMdsbD8WBvON4NRwZgyMXNNmeSBiwFJw8spZtDnONx71TJLOvYrJFVXKaf5Zm907KNtKhi", - "KZkUC5LKpFwZoamXVBPBWAoHcsaIyjOuCRdaErWiWea2QA2Hw0cZKMzqcz8GjEKnQIX0M/s1MjyHXS25", - "jiZzmikWR46xfAol457ZdyO5xnW5MnbI8GsEFsgLpXHmZuJGfQhA/WBB7dVB/dANS7FEijQA9tmzJHvY", - "H5rsMVhTc49+WjLgRAVTZabJHVVEseKWpchioGeF6JmUGaPCjBCy205FCBUoxxK203WAUFpaTo3l1oV2", - "jbnSRJc0y9aGxaZkZ0XXVhjhWqyEYynhczKnWTajyQ2RSVIWBUt3t+GaDQLzq8P5xQGiO8nOH6369tCc", - "X5VFx/o+np86zceJZLaasdRYISMvWcx+8YTV5r/UOp+MRplMaLaUSk9ejl+Oo4BflAXvYpQGJYYy84LL", - "gut1jX19iqQQ90a9ymgURwtpVlmf8SvsT1x/Igsj1g1Dwz3KJAUbCtRRCd1oRlJmVkBUzhKjXhRqOBVv", - "YKoTcm3ndG2EoPtjgj2urU5m+9uZAvO4pqWW10MjERzudMGZIkzg/0G9M5MzmkapWHhS6S3lGZ1ljNjx", - "/g1HmIpErmZcoP4E3Ep5Y2fJcI0w5rffWlSob78lOynLDVakILOSZynRdKF2wSS6XshrMiBnZcHIW2n0", - "claANNih2R1dq2oqMTk6+0iMSRgTlck7pvQuQDCbYmCAxDgvheYrRnYsnSoyMIP9OIW9iz+cXxqraE6V", - "9v3vM2q6v5XvTn8mP58edvS9z2j88+mh6Xp59nF09PH14ejo7OMurvQ14EZ9+y2uCBBPBuSw1HKQMs0S", - "TWashtUdu1M4gaRMYQbv/3Hy+uSQGOjk7dlH/CYLtsoAXG7UoyNZsHen5nAnHy4sNuC4z9aAAISo8xKX", - "JBcZM1MmO67Rz6eHdlRs80YWCfOoxQUd4znyK/qE2EN78j6j+I+FnEafDYjLYu3oRCFhGxRURKlRpQ1B", - "TcyaPbyJzkv8A7+5v/wIZwUzbOzt2ccYOBcMZ6j8yKLJgwV0NXojrmBiFn00SVjGUJ+e1lTeT9X08KCb", - "uYG1jBOzP+K/kAN41t5Wuxps3Fo+V4olpeMum+QBFXqerQcLeZXxGZ1fqaSgRvG8kjkThmta4/jCwqsU", - "khvG8iua8VtW1zFfrFoa5t/kHWohWhLTzSldhlMZWSHIiq1ksSZ0bszDjCptKI7sfMgyuqIDM2+q+Sxj", - "u8OpeIedacGACIyJnaBMEhYgggH3UuosMTknXNBE81uu18Op+KiAG6SBpQa8cBq9WE0jsvOCrLgoNVO7", - "ZqP3lua3PbKUZQE/jM3fjC7MdJHfmqm9DrijA5TRX9a+Dcn4DSO4rCG5YBpbGmhaEnafZzzhOlsTJuAU", - "10YwNp1hxYY6VxUS3KI1WsxlDuxWsFtWeJQM6ySIm9QipkwunkovmVwsGuRi6QMMRilA9At95Yzfura+", - "hfHoQQSc21vSNDNs2izvME25FXX+6x0355iRLyUrWUrK3CDZzAt+uFL8FzacCrsLYxChpci4kWRpxUAb", - "uHveabDS+yvE9BXuzFOXaffTHZDWyVB8VWaaCiZLlRn6/WnJBJI4TBj9p0Y3SmOQkxkzp6hgCRPa6Wuo", - "H3BVnZSd0/OPxDBRM7HdbZBBPghDnvO54bi3zGgHglS8wEAXUgx+YYVsIO6gD3G4xKvVbDukWYzscEHe", - "vdq19j7M1y4KcdmNI5rnhbRo6kURnqvg7DyOFW9uCULTW67MDHHQgVWa7bynogRXLuoryigsuC2GGhUc", - "XaNVa7bKZUELbpB9nzCW4kJuaVYaov1JFjeG/KVYKJ4y0iJAa8cLNlgUlAv0J+lCZk1yHv/wXd/GVMfk", - "qeQcuroACtJJD08IiLfaNXtszbd5mWUxEeyugmt2zZDbi/EBuUDtnHwUlfoDovic6WI9OARpsGQ0ZUX/", - "XuJgj9B53/yn5Xh8wMi4gdu9cb9360rpgmq2cAaKZ19nDUdxuTIqAwiCKI6MNIniaCZLkbI0sIEqLl7f", - "mzNWDOp2gR14DX6mgqdMDck7mqvA+QX7ppeMF61elQAW0rBke4tDcziFZtssCqtx0NMmQzYBSt/JPPgF", - "UTj2O2Dl5+6EhAKU7Jh/GFgpW1GRxvaUWpHP04zttkD/CAJ2UhemFlIlNeOayAS9GyjROQWXxm53S5ri", - "hkyjEAO4KGAzbZm8QxVcWJjTkRfMjuHa16V1TNgtE03W6jGUcyFC4QBzBQYE4lCRFb83K8T1Go4CKLR8", - "gePhUnTFSC5b/ODX6NXh4clotmADcAcNmBjc7g1fRBNPgyte6OUoWUpxsx6szJHXti3cwWRcLEqaDfai", - "CZJrl4cMJ3OV8o6LgldUMZLygiXasFHDtygXlRNClTP3lRvidcYnBWuIq8QQtnLrnRdyhTbGr9WgDyO0", - "8lmhRkZ/P3Y2v/cHVpZOrZv3BJtededIf6eCFbTqdQ5/bdUtkQvBf6k6wt+grj7a9a7g2nW8YF/2L9gX", - "4n5t9q4rrv9nNNSI0ZFrp5gmt5ySW56zYndojpc5/YrpGGw6sLn1gIuG5xgkouPJTR20NU6XSmoodAsJ", - "9H6zvpjznGVcMPQwowuBHNNk6b/AiVVTQUki87VzCUHLmChJlnyxZAWKXwWsbiUL5nSRWanx7MGvUxFM", - "wskIr+2PncYS8sqdFRc778vVkTE/n+82dc69LjFiuUj7/Jxypb1OWfFz277GlDoNrMslU6zDwuCrFUs5", - "1SxbI29yBw9v6mJCbyWHQwQunoFdOsmoZiLxksPOSC1lmRllRydLopdSMeBonoiDE0p22HAxNGZXN2+a", - "Rmba2yumdaMMxmxi/FMfG4wjMHX4KMl4PrjlGi6SBrlZxsH+04x1i6ArzVdMlrpuR4+jPjULvE9akjvK", - "NWgx1FEZXAVIswLNYnv3Z1ZoVTDT3nTeaP4ejBU6N/ZW+H8wdYUkdpYxCQyjc6cBoY4KN1ZGENm2gZr2", - "nLylmt3RNbnEb012cDDuZADq4CopWMqE5jRTT3ZnHFSWaQCl6WV27uEel7KPDugKAQiCEdAJW8UheE8y", - "2QFXvSwIX9EFXNJXsREbw0vYva6HJ2xuf2LAfzw/rfX5/BBHIOB6b6S4yMuO1Z2Yn/0KtcQFhdEny4Kx", - "RvyJWLjQDheEcj2NlizLJLmTRZZOo+ta+EnQVE3AzbbEIBJDe7bH5+tNESsVxncNgEeDVCz8h5jUmsLW", - "GDLA9sGfE9PQ/msapVTTCXwd5WLxv8zx/+55PBwOp9HDg5lpnY8ESze6E9y3g3leGJXGkFx/mEwTl2Rn", - "RpObO1qkJGDZT4mdCbHdC21rDtY7THAIGptVOwjqj4vR6bm/em/UXCvNnUCxgjG44G/Imxpf6pUDbeQU", - "pUiorqsquihZ6/rbNiRw7szBmnNtZwXoutckY2Khlx33gg3W5e7F8Ah3MTB79DuvYj2HUtHk06fxcLy3", - "fxAPxsPx8xffxePh+PuXP3yOze/7B8/h9xfffW9+f/nD5+BOtAdFrUvScLRe0qmY5i2o+IrsSMFAcUN0", - "IcJrlOP/8dhlfVsGb3kPipoLuIgMk/eT/E2k0rOFAXo697EoMJ6jgVT3c33C0NpFVz46BQTSNeobLrha", - "njOqDNjmKPg7uVuuyYIJe91BlJZ5Dpe7zn1hfoniqCLqKmizurOY80yDgTkvBTgkoUWnj+ONbfGazTla", - "RO25Vd8MWVHioOKlYkIFmTEIDsVbK6/vt0KfamDbGkDwtxnozkDHoCw7XipZLWQnesuwhbMR7hiFCAEu", - "IFwjASx2cRijOXfHvYmAz1ULlbC+2tgLpq/seJ32Fi3oihnLsd8r1cXS/uPiw3tyAcyaIPFYvMxQJwxm", - "FQxR8zvUke7R0B3mx/UaPBtKU6PqGvuAXFBB3hRUJFwlMiZHh4+SvR+ki/JbPxggSV1Lt/Er3QEfRmES", - "c7j8xK7V0o0hydPGPvexeNj1rhm+xSPHjpaSJx04nMPZvSr84d0kYWsH/cEIlJTdd+mEKbtHSuOKJDCy", - "8yllXOnOeM1MLvJCzjrY/qlcEPOJznhm9jSImjVGh7/8Z2lM7HW9KDOIBDf/pzNDO0iQbR9TFRO/aeEO", - "iy6EvhUkCoiowMUNvG7emc7I4QtdMLoyJyPgmjby9uLiGNx/mhhFc7fFjRDldbG3zfIwngwppeviuGBU", - "szTQ+lrxwy1m4WVlO/IRMRE4sJMl1UNrm3Iphhix9nmrCF0LrZpkEA5ksfHoJvSdkZRlmm6Lw9fQ+CH+", - "g05WE9E9hIdT3LTC124RTQmYaeoVcIiAdYTXJqu+3INLecOEB4Jz6T18FQX8ptSRTSkcOLSqL6PKJlFP", - "zdjwe9lK2+hF8zZJNm1ZZY84S32ui0PmjsEiOqoqFBCuSF4wxTAL5c9HdD1XBq0SzJOx08IZ144ARE56", - "4H9i9ozbml7XxoredwUl77/4rte9hrHIWvqdC/UU6Nhxp4dbqzoTAm5ZoZDHu2YuayXwvD45ayXI+tre", - "jgntYbs82WcQ++8Ni3gBMV+jBVut6OBgsDcbcN3puoNT+ri+dDInQNvgIuBwU67sHTmceBfGAbRHFfES", - "UnWGzWq2ys28y6Juie81x70wCwImUvWAQHDyI4SUFStD4ponsXP//4he/4KKVK4aoeLfx9tYn3BGvCh6", - "9GxU4lrKrkPrXP3wmew4HVvtBmfXWDrmWDoXudeozBG3Xn64Lq8y+HAhPinExQ9tfaY7DW2ZX908kgNx", - "KfPBDVFuX3Z8SO69ZkJxKWJ362yTvg7PTroznsxo+ebtf18mGStVNVygh0Zbxf13GfGeEWzmVpU3pj6p", - "NltIwFr1ipPNklRsk2LYTSQBENuW7FgTNFs77XpvawbUsD4265SNqGvB7+GOQGm6yskOBtyrXSsLjYFX", - "zfWOKlIpftWd2Pfj53v7B8+7+XFXxtshKQX/UjLC4SJgzm3YNKa91bBc43fmW7LKswGdJXv7B12sbjuv", - "UaXsd2YLeZ25rUNYk9r08XbQtKlTT6PA49L41ulFKZ9iHn3sNo621M/dYJtOxcc+5cotoibIu69/rfDm", - "TSqKum9Q5SrXT4aK3XrYjqZZL8BL89UBwwwKhEX+LZhqF0NrYL0+8bgDRY2pdKHd3RV12PjmC4HUiAKs", - "UPLx/KTFb/rzKapOZKf3tqSeDdLdjP/j1Yfzu/F/vl3IbbKR+q7wum7FehYdGkvB9d0mba1xleaujR47", - "Vx79nnFWxnIF5FEDGb7GnT0qBGDEVih1mqIDo1o6eKaPqEuawS6WDfkM0ymm9Eyj3foFmEv02Tps6El3", - "6P7WZtPcWTO8Z7u7nw1BAN0fJvzlE2d/rwsKtxybpv/2lL8/Pie+sd+CX1ghB2op4aLdxwaBMpeUSssV", - "yeiMoUHWyD8wtLwqBdfr0SLjghVXuJLb/eGeTUZoNcGN0lTdPG2VlSGxaZW+1ej09F3fRvXYJJ/6jBKz", - "jgITdkZv7bcP79///NRADReJdcXFXG4K2NxohnswcCBPDKhWwOZrpinPIL6vcoXSmY2msP2rYD4X4pHQ", - "HHVXztRwKt7RvB4CBKHvGP8t5nJIPkIEEpjt3thheHkRgpoKRpNlOLLPxtpB2oqJoUJDhDEpWAbzVTGh", - "Qt2xQu22wgtTlfHVCE4/RM68Pz7HBMpqUHDNA/Do88PjtNjfO47c3CKziXZyURzZ2fWDD0/DVvA/d3qO", - "ghC+TaTf3tgm3YexgHXC78JnHP6a0WLhfn50rU88FjaucfPa6uGOPQzWwRrc7g9WB0+dh4123DSPviDI", - "NqZd6GR9ptnqy2I0z6gY6BeW66svJU0HXxZRXP8K++A/PmEpXcmveE9WoToUeTXWWqe2ECtdKoHnRcdC", - "2zyxVomSTTU85HyumCvYwQBGVy2SIBa+82LInKOOy2uEZ9qTnbPj85h8OH8bk9MPRzF5d3JxVNcdz4C0", - "2+6nRBasp0AHlgOAFujx0ZK0s/9/+GEbnw5EPz4VVTO24KJ2FsZPK3Ri4djiJRUy/kMuBblY8TB2ZFPy", - "vtsDtw6ofRI57G2knF6fq+WObZSgTmKnjq0w7w0UG7JjFR17MqXI1rsuIaRUhoVQEjaJyVqW4N/CtOY1", - "oWLtMbPOWQg9ngpIqjNgKr3JwDL6khGvKB4LpgsMNx9OhQ10tsVTbGoVtHumfPCuXQjkDKlWrt2nKGeF", - "AgsUzAexRpshLcFkTqlmT+N2j3h2m7Jka1HSI0naFw5Wjl71bfO5bdDaArIzr5RX0FKfeaH8rFI71j6a", - "F2xk7xNy0f9W/3BdHXS4Mnbk4ZSDmJwfvzo+3d24lw4CJFBUSgzhGCaC6cWtbZ3bHJw4upPFjboC7gAR", - "BSy94uJpm2rOouqu71HDIBA3Zyi26vOpTj65w+wwbdOzjeWMud0nIhkSDp6MoxJOrJDD3yGhnJsHp/8I", - "t+izPt2aNgWF4ZmGSbQCwszg3eFgWynjVgD+9tgwd/JWdW/fI8dn03I9aW9YsDsjtUs5xyVxKhuO2MaT", - "8htwiUM8jsS+0DdHAhtJqDjy8+8QLtXaIMCLtjmhYx0QqYzca0KsrhEykffH57tkUHEFaMpSonIqFKZw", - "15QSA84ZApMqOpoWM64LWnhRR8OcFKwhUZN4kMjjt2tCzjs4nKsE5RlB70YaYNbOmZBjC+EW0iqVrf9l", - "PoJ/qwljF5mddUdtbU11kHuXzdsRqNBr87a3sX3xUTPOem8/glZo+fbSR0Pxb9t68RNZTEi4W0Uc+APV", - "YpZLhik/T2RtPYq20ST9Ti55Tqw7MSjT46Xcb1aw3R2L52obNO4XW92iUp49GQcNzgN4tKACHXiD1msM", - "sP4wg8eUspoR3KGTuQzB7oDihnXcFbgJVwId9H9WmO4iZQVLgzJgTrFAyEMCUZUZh7w5V4pVOU+3Z0Bi", - "MRVUpAQB8rCw2JyzLFUjzVZ5RjVTZMbmZvcTmmU+L4iJNJdctJWpE6FRJbZxq808CXT/v4baCPYnMi9F", - "Ss3YNINiZU9SuL6UrFh3VcWkRbIk8BXrwrKM3VJHzM2I7+Y0Cc3zjCeeM24X+I1zqfZwE/31KVFP0008", - "Pfae6269PkAFC9QSnHiMScW25JNCZaWmmD01PL8HX3aC3WgCp8fmTCfVl+oUnAoAM9IFFcrMuk6tl+Zn", - "Q+Wm7VtWrKiYkI5Ku3+oYdftwNrSf/WI+2o7OrXI24j3P4o+Dbge+uyxlXAK2hiLsI071KnTqEXHpKZF", - "x6jtGKvI/DZjdNVtRDxuu/1OC8kG8HVIZpkxzF1wEVEK+C4mCiRBYFmYarFWmq3gwh1C/H1Na5t20aml", - "NfMMuwtNdl6OttSxDZUqm6mEnbvbuAttlJjcfA3aW48SopI6IkIgftTHTe2i6dCOnWot0vV4NPa3nagS", - "rLG95eDytAFdYXQOxF9O/bDTyEhpH8m1G+y/n9m2OOvoUMcbRGj24Q5Cc1sBo78ZX248h7e+WJ6PPZE8", - "Ng61nfcCcW39ATxb7gisdtO2/Lad4P4kbLkdPQHfJyIpGGpEwXwhGGNz9Pdv3R4fRL3dHmEYMZSJKJQN", - "Jq82DuLdO2v8P56LEr4rYMN0kB93etWfstkdU95+k3s38E2A8PY0fK7UNulptFiABt4hEiEry6YOWzT5", - "1rbEjx+rdlp+nfrcKEx6buVWTaOHPyBDDde3fYJaV2ZUHGDg8xYo3+Ls1Oa3zfnZsAch3Ar5wc5sj0a3", - "AsRlH2VuS4U+CrllvutCZgoU2UoSYvlWKTM1JB9ydFBNxYBMI1pqaYgEgx1TlvCUKXJXJcEBEqkVaWFZ", - "0mkkpGBVXwj0FlLbHmKNA2JTt+9V81WpXFNti7kZDc/0MV1wuRNbfpS60rdJLT/Sp4A2Hqxwx9ssLoph", - "nlFAe58fT7nfbtiNnLj+5Tcnf25xhvrzH3/9KorF54c4+gcrFJei32SAQr5QHKWjRhQW+XUhxDUOsj/e", - "fz4Y7w32XlzujScH48l4/L+7DtmC66tErlZdRbvfcggHXnFNllQta/BRi3jeCVJe3eKyOkBKYiORiGtT", - "z6rYG+6/GI67wPbCdAHyXQBv94bjLnCNzaq6BviIQ+TXltVFKc2qKy5UsKq98tcDKn89oNJPLz1FZLuS", - "hRetx0pAMvtKVFgKUrXoJWO3LPu9xW1PAQjs0vrxpLvHoF0AkE75vN1EauWiIC4x7kHYLStmhmTWBPFQ", - "qa8pm5ULcO1A9zuKz7O4chAVN7ENWqxpu1XWporBhjTrna4sdV5qG2JNANlD8sx1w7dcEpnJgv+CdU2V", - "zFhMnv1TSfHMaWplosuCpaBpxeRZJhfzlcav+AAMm895Ag7vG7b+ESq7kZzyQsXkmZAyd2/GZEzoYYCy", - "YPr/xLgOhA1qgszraAsaP4q6nmJV7ccMkoQpdXXD1p1PrR3+dEGwiVkYOXkdFGy6YWulZcGIWgtN73GF", - "LCmYJpmUN2U+JG8gBdRVPz/86eLq8Ojo+OLi6j+P/+vq5DVh4pYXUoDP/5YWHILruC8IWH8aZy3LYoCT", - "Gdyw9YB3unzcrUAHjz0IY+5dO1cP7pk6GNIV/UUKeqeGiVw9I7IwW+2eZPhhPB7jNr7j4uRDPWys2RlC", - "1MUpligJSu4FjnHA1FWF/27kW4RWe/B7N+Di+Oj8+DLYh9+wCThIsBednn+mlE8i6UpOsxW2cZXQFg8T", - "Hitbs3hNgoptT1p717RhlAHOqDOHiV0plT1ac+kYC6pfXJyOLk8vYOyLA8M7BD4doLxpMoFsVGhx+NMF", - "vAbg60snNKtIqaNsx6OcfMv6+u0zj4XOrwxZq643Zrhmmb3ptm2JaQu1FkcnZ5g3n3FxQ1J5B3VmFdTx", - "ZKtcr2MIfALYWFffQjBqEcs1yQt+SzUjBg6fk1kmk5sr++MVz/HhnKJku8P6PYn9pz1dSSqG9V/2ftgf", - "jodPjjt2yMipXm6LDNOW5AWb83vmimNnbDIaYRmZA/Ovj+enLaTAGCFShsag9J1LxQidKZmVmtm2ljmN", - "PipWqFFKNR3tYiccxXSZlckN0yOcj+uxWg/s72UOGzRq4jOEadhVq8PT8Njax0dP0SvTo1bduyINUlAB", - "qe17+98by2M4Hr2Myd44+Pf3+8O97+Cvvf2YmN3f++4l/v1dTPa++2G4/+K5/Xu3M7nbEa+rmXllU0dr", - "Mz9oJxbbepRm37lI+S1PS5r5o0BkbhMzITTOwQyL149BOvCV0QH2+gqm+9kp/gu7mq01q09sb/z85Yvv", - "vxv3VlA3/QzVOkC2bDtWsyUIsBZU7eH5yY0fsTW40N89dxPGtLGUrzC9ul4iYfz8Zd88oR+546lejpaM", - "L5Ywv5zfQ1oS1vbzefsFM8uqP/+EwDdhtM1NHx6snmorblDMlkW/SHQInDaKMS8RnmdSk9FowfWynBl+", - "YxXydDayZYfbuTfOjMBC/rY4LsR7IeuvorfgBa/CPyNnXxN8d1o9PjAV33xDflpSiKewgMOnkmwcqnJS", - "5TSAji/c+BkEKtDh2Qm43b79tipZ/danFX/77YTARRs8I1DVYNw5Oj05222lwSEg6OBKWRsIF2xFheZJ", - "lfIH8wmfy3NvBA6AYN1LXwjP17c2sKqYgYINbASFFfwQaIGPotmZvKcrlrqAvKDatQFkA9gIBlOrmMhi", - "QQX/xeUbOXe1exWT3etgeefuPhkw5C710WTGmEl3u+5S63yUXJW0HduwkPAXppMhVtB2rAQKYVHN4GmJ", - "jFEjIDRx5Ig0OORylMpEjbws9vQCtWAgO6uDZhIqSFEKY9mLlGZSMHi2sqosTwXBc0CSrFSaFUAsp0Bh", - "1f41KM0wRnavWQGq3dmJfZuCiQQSwAYdZHg9ojnHrMnrSi2v3QtDT09K1VN1jgDOD99WdblN25BUXLQU", - "8OuVOSosdSTzpaQZ12vT5YgJXdAMzEAbwseKW4jDSQqpFEm5kX6zUrOUCJniQGdGZCXrAUTOYvPa6YOc", - "ZVuSP2P0lilidFHToqDesty1W/aGQeEQu4PfkK5ziZSIWbiGAsOjVKta7x4F7K1Vj5AOz04AzHb74o4l", - "+lkJvvoG83jFhVHXvVM+BmvZreRdxUCsDm8ZCQLEhGm/XABoPpPgAZJ/Al2YzR+gyKhYkMppwiwkCL4J", - "5wXp3z6LXJGd69488utdcwaM6obAbKr2kUeKAfhRMdWoRGu9CzvXv6kIsC33e21xYc7rEVXwnhlBxCC1", - "xgQIcYBoLJguOLulWUxuuTIaiOIrntEC6BmxXmPHTcJ5UzFdf5haz9jtkn8LKSyAQV5bOlsbYIe97yX0", - "PnqAsI7w0WADY3+Az2SRy8tTVw0fnmjElm+sZIC51+za5kMBLgRoTnnmz5IXJduuITxYHQsJHmJAiH8v", - "DXv6xQvPw/qDYoZqvmCT6m0gPq9QHdCv6V6LtQSWZ0P6yI6NrVxSkWbGAOEsS4O4bgy4znjC7FWI02qy", - "jJwb/UqRc4bPhrZUnEqmZGxBsfgO1/gKbPXMdBRcI0S3ezTLl3QP6o6gJRpNooPheHgQxZG3q3DnwRqV", - "qss9k2fcmAFmpR0vyFrRqpwQqGsNjYICTmV6Z4kWKQAIvnpoo03rwaP34bvO8Kqtrp7BxCeVTeOP7m2D", - "H33BAvPzG6pQbapVfnLTALp6549TW0Pyb9w4JhMe7AF5t4nXB8F79ZP25BND7Mk0f52jZgWmK5zI8Dkx", - "w5v33Qt3Q+jGwFvoqznMpF66J8gNWsLH+t1W2UcczV/X19f/VEbQ/ToVpFZtvechYmCgMTbGYZDFCkLM", - "T7A0BGA3KXafai9NmyYvxmP/sf7kNn71H/0j1wh4OhXmv8h8fpiKB1gF3gM7c/AkdW/gXuIlhzV9X8l0", - "3Sj8F0T+jv5pq0LiVcBWb/K6iNWH+hWLMcLhBxTecCr3x+M/emx7BQuDd8XtAVmmRJXgPpyXWQYOhOd/", - "4EywvHXHDE4EFMt1Xgcz7os/Z1yrEltbj9mGcaTK1YoWa0caHfxPsQWGg5jmqI/1c1FX/0jZiueBDm0d", - "FuEzDMhTs4ZKr4b2CSivjHMVVPW3Fhzoi89UXU20ihB6cO91jBH+UHIthRxI7NuqgBPYlzYQEWAEKmCb", - "pTfe7tzIC8MXmqo3a+zbJcbKwlVgj8AK0RIKEQdv4QazcRbhAIJnVlaxch647qouuxNymIAfNHw9waok", - "FRKawEBVrnc1iLXOkVtOK/3W4qmmB+OjXsH7GoA1H/L5VV/b6HoYqP8Fjq0e3UCF+es8udFRn2n3dwrE", - "4BUpuMlIwndFKbyHnZbIclg6FVbRge2YZ+APwxeXbvENqLQUKRUaEnPdqWgaZOZnf1fiY9N9yDTkUhuq", - "sQSFDxra96AD7icTzfQAQ9WuvYmnWMGNoWwTsKzBF5OVVJr4m9DdFjTDS68nGK7mhBAyhKC0DPCLwO4P", - "lQOkY0N1YTmtFnVN2ipEoAL0Pftk9QdgkKblpwbtG6JqpuhMo8+VrJ+Kd53P9bTpafMEN70J1TVNUEge", - "PTOU5EupJaYAJFSbA9TR9fedIltjzR4mA/7zBl3IyarK7P9KSlHtwaI/WSmqv5hixu47YHWYjbsSOHkD", - "d/JY2n7aJCwzOoPmHRE3LcWkwn1QW/pfRTV7Pn7+9ce178hJo3GUIv2XUgndCQk4ImqBvpr0Fopg4JhG", - "TS8oiWbVC6hmRg52h1Pxxmx6S9szKpItk3u0pJoc+SKRihyenfw+5Syow/YHamfffEP80wggDpkm13jS", - "8Fb+GpPz8FG5C9iEwYURj8dQDxpeT9jF5fMq7noqMEgB60nblx9RNNvHRY15em2wVCEJVPtrG0WMryLa", - "AHAMGYRB0C1JPr3+8P748/WwQ48zP4H3saeyr9W+UaS66uBwQ7tZ4nTVu/OSxgGqC5tCZlZiYI4Yigt7", - "YvDDf8kS958sWZbPy4z49LFhTfpUsErFig5IkA35/00jlChuXr4Yu2m0/+I7+zvi1fwGwRahfK40CbLz", - "XoqBj8HftW6HGVXLqdHWMjL4mZx9uLiEy5bJaFTFAb0cvxwDObozSKZTM/Lgb2TqHvgbXK5zNiFNrjGN", - "XNuUPOtyT2zYiNZWbIM4d2XY1Fv+fRo9fK7A1jC5Nx6DE+NZJ9ou/l9Hmae1Cj8VRRm2UUNOr05z5INe", - "v4ZC03yp4E/WaVqlxx9sGPAIGGGnPrP1szJdsvKop275kJwzXRbC5sAYw0uE59qbWfFUyKKLyddfAPFW", - "2U4oKKCMzV9q0J+qBpnRD77+6G+Dh+bsy/9l8PL/jpBOU6reL9jtU9ICRcuoWB2GHypvtnbl5NdowXRX", - "7jmStFFtFNhr7drDsffdx9UNd1yVvY3J++PzmGB5jVqNSu9vdx4x9GS1L7u6vWem7d/9LRaEi5pjtKS3", - "jFwP+Mtrosr5nN87l4m9g8BBDvuqPJMd9+g3eA3OslJBytbGWYX3G9YJYm/ktlhS4/bueJXrNQa9tjfc", - "gn/r1VSE36ws3KfSbgu/qvCz3QKCB93h8X+H1XrhQ8PgOks7+1FtiQccsxG60jGsfwzeNL98EZM3p4fv", - "B+Yfrw7PL5HmTt/9/S3piHrxt8g1AyGwDYz2b8+he1fcwEMX27BD3p5ypd+5WrBfTeA1ap73sU/lLuX/", - "u2zoV/Rf82rjtMtKQ2bo6bjflPUHQ4GLMA3qlT0awWWjqqpALsug4WyFhc4b3BHjxoxOir5UCJIqUnIk", - "35+e1sqC2hC6s+NzuLWHCdln6f1T81U5xWlkozw+nL+FsJtw3lUHrLWI6mlVbdF3Pv1wZDqf+jg13/E9", - "uyP/JYsb7ApZ1sx3e3dyAf3ecZWwLKOCyVJ5bLZs+OqqeRsrvsWQvP/5S1te7EDHK/5yKIW4h+kdNVwO", - "fyshgecNTRh5dXx+aS/j23tmOtd5nrtCSrpqxN5yij4F/Pu6ekjz8ZvowO7oKHDqTWaoz2J9yBsqaTa3", - "t1VMs+ZebrG+elUyzr6W97RVo/dPtjbaVT+7/JiOJdgwmL+8mP8T1Pcgrve36++evhrSxcknoyhuEk4D", - "W8Es7w3Y8gE9VQE4LQm1IcA1gVSvb+/E0REGfJ27Snr+QQV0Jdp4sFWp9GQq9oY+wNmO5wrqIdN2UTNq", - "KvaNAQ1VoKBAgiu3NxUHQ3LBRNqxJvfeP1Xk2q7vGufBFUmZ4gtBbhjLVZVHq6lmGVMKVDnIfFF2ypD1", - "gVza4L+qBJjJBU+Gv1si1XT8DnnkvODY68p/AKmE8Zj1KL68YHNWdIq0eijfEyRJT2FGL02ASrDpxhKB", - "voPdlsBhO23VRHxnIZ1aSBMCG7goecoIBkBWUVUGAJRLdK3Jm6Bc4oS8Z2VBMyKYtiKuYNC5cQE4FW9k", - "4cnPy3NH8iD1m7GMMaQ4aU/kA8VTNhXXGZ+NfFcjw5MbyCJZ8mTpAyMrenq8fmRTthrQZ7aG4teSq2EV", - "0D9dqNZKQHbwVrt4u0F/ydP/GfLUHcbfIU0NiEr0rSuh5wQqeBA2SdRGvcxNqTUdDggw+H6/7Kj5ORrD", - "gruLiUSmrABZEZOUwV8DLri2P2EEcNUIEj1Vl62zhb/km2+ITwxwwWd/dz3qWVveIekgWsGPpbUHQEJG", - "O6G8UB5Qse6Doqoytu6xgFCHAOOXFjRfFlT5TC3AngtmJCmfQ0IMBC2lLlfMFj51uWZVHVTTy9Umz6hY", - "lHCD05KpRpp0Ia1CKaJv8+XnhmKm9WgbZ9EtmuidkGk5Hh8kywz+z8grtpZGyev5WWlagLNLkWOtKfkP", - "cBYYy4+mPMtoAq7AIlWPGoCA5a8YTdyogPuny6h6Hdi+iOLCF2v9S0T9dWOzlYwK2FOXXEFJFVTK2nhR", - "0yicFZOFL/gVk5kvLoYCoV25a9hxiaz/4StpfbXj1ayZ1oFs2yQsn/Xf48tuXLhph7v6zKAZEKWCwnvd", - "CdmHZyeWdH3204jmPHr4/PB/AwAA//8FXdNIGbUAAA==", + "H4sIAAAAAAAC/+x9C28bN5foX+FVLxC7O3rYTtJUF8XCcZx83nUSf7bztXujQKZmKIn1iJwOObbVwve3", + "X/AcksN5yXIe/Rb7FSjQWMPn4eF58Tz+6MVylUnBhFa98R89FS/ZisI/j5ZUv2VK0QUzf2a5zFiuOYOP", + "sRSaCW3++b9zNu+Ne98Ny5GGdphhMMaR7XEf9XKZsod6nps291FPS5lOY5qmU56YPglTcc4zzaXojXsn", + "r4icE71kxLQjph3RS67ICiclXJGcqUyKhIsF0ZLsSJGuyVzmxKziJ9Nttxf19DpjvXFP6ZyLRWVe1Zz1", + "0s2lyIomjMzWsASqFFeaCl2fxH8wM3HNVuqh3ZsZjmiawkpwaTTP6bp3b8DHfit4zpLe+COC8pNvJGe/", + "shhg3AL5xj7sd2IPc0AuiiyTuVZE30qz/BXVajwRfXLBV1nKCIJnTCa9v7E0lRFZyltCc0bWsvj3Sc+0", + "PDTLNIdiByUZNQPuvM+YODwhqyLVfCUTmtrxd8fk4x8TWP+kZ0bW7E5PepH/l59t0rv/NBG9qCcFez/v", + "jT/Wt2MXabq52VtOtt7r0Qve9gwt2M9orpvH+AmOqBDXzVM5JLH5AIhttnLL9ZJkUnHznXCBy+BSDHpR", + "7VYykUzjJc2bgx4taU5jzfJwJCJzvuCCpnaiJcuZnZyJRJEddhenheI3LLgiXGi2YLnZUtuNvGC/FUzE", + "jIhiNWO5u5446s4oInsR2Y/IYDBoGTPq3fUXsm9/LbjQB/tmIqVprr/SzmAs1bof07bltvvldyJV7VLy", + "pGcHqyw9Ks+n/cYW4vpIijlftOwSfi9yOHigLLAkQ9TMzEyZSyvJJctXXDNyeHYymIhLQwm5IpQoczX4", + "nLPEbGLOFzCEOZi/XV6emeakTxI+n7NckXkuV/BtXqQpgWWxHBcwEbdLHi8JF3FaJEyRLJc3PGE5USxl", + "MSyOioTENF6atcXhsgdwe6sYu6J3U9iJJbNzWqS6N342iuq0it7xVbEK0Aq7mV3nTBe5GZvdUUMCsH/z", + "fFcyYWllnt6c3zFzWh1HbvYAvcw0hWIDcsz1kuXkCXR8AmBUlu7Iayb6M6oMkG3niMicUDuEoCuGwIW/", + "1TBG0KrhH+bT/XAQbsEvrcGa5A3LU5pNYcKH4PbOw8t2y5ARmq5kxvQtY8KC8mEAKpbRnGqZV4E4EXCy", + "NRiai+c7AKBgRx42lc3aIZpsmOYLptu32tjrJTQOKQ9uM2P2ulR32LpFvcyZWso0qUw2GjyL2m5kAqTO", + "94Fdvn/37hd7wmRnNBj19waj3XBmGAypuDnmVNKApODigaS0U4hzvO6tIpklHZslspLKdJM8c3ZaNoHW", + "K0lKKsWCJDIuVoZp6iXVRDCWwIWcMaKylGvChZZErWiauiNQg8HgQQIKq/rUDQEj0CkQIf3K/ugZmsOm", + "S6574zlNFYt6jrB8DDnjnjl3w7lGVb4ycsDwewQSyHOlceVm4UZ8CIb60Q61Vx3qx/axFIulSILBPnmS", + "ZC/7fZ08Bnuqn9HPSwaUKGeqSDW5pYoolt+wBEkM9CwBPZMyZVSYGUJy2yoIoQDlSMJ2sg4gSkPKqZDc", + "KtOuEFca64Km6dqQ2ITsrOjaMiPci+VwLCF8TuY0TWc0viYyjos8Z8nuNlSzhmB+d7i+KAB0K9r5q1U9", + "HprxaZG37O/D+amTfBxLZqsZS4wWMvScxZwXj1ll/Uuts/FwmMqYpkup9PjF6MWoF9CLIudthNKAxGBm", + "lnOZc72ukK+PPSnEnRGvUtqLegtpdlld8UvsT1x/InPD1g1BwzNKJQUdCsRRCd1oShJmdkBUxmIjXuRq", + "MBGvYaljcmXXdGWYoPtjjD2urExm+9uVAvG4ooWWVwPDERzsdM6ZIkzg/0G8M4szkkahWHhT6Q3lKZ2l", + "jNj5/g1nmIhYrmZcoPwE1Ep5ZWfJcI8w5/ffW1Co778nOwnLDFSkILOCpwnRdKF2QSW6Wsgr0idnRc7I", + "G2nkcpYDN9ih6S1dq3IpETk6+0CMShgRlcpbpvQujGAOxYwBHOO8EJqvGNmxeKpI30z20wTOLnp/fmm0", + "ojlV2ve/S6np/ka+Pf2F/HJ62NL3LqXRL6eHpuvl2Yfh0YdXh8Ojsw+7uNNXABv1/fe4IwA86ZPDQst+", + "wjSLNZmxClR37EnhAuIigRW8+8fJq5NDYkYnb84+4DeZs1UKw2VGPDqSOXt7ai53/P7CQgOu+2wNAMAR", + "dVbgluQiZWbJZMc1+uX00M6KbV7LPGYetLihY7xHfkcfEXqoT96lFP+xkJPeJzPEZb52eKIQsQ0ISqTU", + "KNKGQ43Nnv14Y50V+Ad+c3/5Gc5yZsjYm7MPEVAumM5g+ZEFkx8WwFXrjbCChVnw0ThmKUN5elIReT+W", + "y8OLbtYG2jIuzP6I/0IK4El7U+yqkXGr+UwViwtHXTbxAyr0PF33F3Ka8hmdT1WcUyN4TmXGhKGaVjm+", + "sOOVAsk1Y9mUpvyGVWXMZ6uGhPk3eYtSiJbEdHNCl6FUhlcIsmIrma8JnRv1MKVKG4wjO+/TlK5o36yb", + "aj5L2e5gIt5iZ5ozQAKjYsfIk4QdEIcB81LiNDE5J1zQWPMbrteDifiggBokgaYGtHDSe7aa9MjOM7Li", + "otBM7ZqD3lua3/bIUhY5/DAyfzO6MMtFemuW9iqgjm6glP6+9m1Iyq8ZwW0NyAXT2NKMpiVhd1nKY67T", + "NWECbnFlBqPTGVJssHNVAsFtWqPGXGRAbgW7YbkHyaCKgnhIDWRK5eKx+JLKxaKGLhY/QGGUAli/0FOn", + "/Fal9S2URz9EQLm9Jk1TQ6bN9g6ThFtW57/ecnOPGfmtYAVLSJEZIJt1wQ9TxX9ng4mwpzACFlqIlBtO", + "lpQEtAa7p60KK72bIqSneDKP3aY9T3dBGjdD8VWRaiqYLFRq8PfnJROI4rBgtJ8a2SiJgE+mzNyinMVM", + "aCevoXzAVXlTdk7PPxBDRM3CdrcBBnkvDHrO54bi3jAjHQhS0gIzupCi/zvLZQ1wB12Awy1OV7PtgGYh", + "ssMFefty1+r7sF67KYRlO4xoluXSgqkTRHivgrvzMFS8uiUITW64MivESftWaLbrnogCTLkorygjsOCx", + "GGxUcHWNVK3ZKpM5zbkB9l3MWIIbuaFpYZD2Z5lfG/SXYqF4wkgDAa0eL1h/kVMu0J6kc5nW0Xn04/Ou", + "gymvyWPROTR1wSiIJx00IUDe8tTstTXf5kWaRkSw23Jcc2oG3Z6NDsgFSufkgyjFH2DF50zn6/4hcIMl", + "ownLu88SJ3sAz7vWPylGowNGRjXY7o26rVtTpXOq2cIpKJ58ndUMxcXKiAzACHpRz3CTXtSbyUIkLAl0", + "oJKKV8/mjOX9ql5gJ16DnSnnCVMD8pZmKjB+wbnpJeN5o1fJgIU0JNm+4tAMbqE5NgvCch60tMmQTIDQ", + "dzIPfkEQjvwJWP65OyYhAyU75h9mrIStqEgie0sty+dJynYbQ/8EDHZcZaZ2pJJrRhWWCXI3YKIzCi6N", + "3u62NMEDmfRCCOCmgMw0efIOVfBgYW5HljM7h2tf5dYRYTdM1Emrh1DGhQiZA6wVCBCwQ0VW/M7sEPdr", + "KAqA0NIFjpdL0RUjmWzQgz96Lw8PT4azBeuDOajPRP9mb/CsN/Y4uOK5Xg7jpRTX6/7KXHlt28IbTMrF", + "oqBpf683RnRts5DhYqYJb3koeEkVIwnPWawNGTV0i3JRGiFUMXNfuUFep3xS0Ia4ig1iK7ffeS5XqGP8", + "UU56P0Qtn+VqaOT3Y6fze3tgqelUunlLsOlVNY50d8pZTste5/DXVt1iuRD897Ij/A3i6oNdb3OuXccL", + "9tv+BfuNuF/rvauC6/8bDjRCdOjaKabJDafkhmcs3x2Y62Vuv2I6Ap0OdG7d56JmOQaO6GhyXQZtzNMm", + "khoM3YIDvdssL2Y8YykXDC3MaEIgxzRe+i9wY9VEUBLLbO1MQtAyIkqSJV8sWY7sVwGpW8mcOVlkVmi8", + "e/DrRASLcDzCS/sjJ7GEtHJnxcXOu2J1ZNTPp7t1mXOvjY1YKtK8P6dcaS9TlvTctq8QpVYF63LJFGvR", + "MPhqxRJONUvXSJvcxcOXuojQG8nhEoGJp2+3TlKqmYg957ArUktZpEbY0fGS6KVUDCiaR+LghpIdNlgM", + "jNrVTpsmPbPs7QXTqlIGc9Yh/rGLDEY9UHX4ME551r/hGh6S+pnZxsH+45R1C6Cp5ismC13Vo0e9LjEL", + "rE9aklvKNUgx1GEZPAVIswPNIvv2Z3ZoRTDT3nTeqP4ejBQaN/ZW+H9QdYUkdpURCRSjcycBoYwKL1aG", + "Edm2gZj2lLyhmt3SNbnEb3VycDBqJQDqYBrnLGFCc5qqR5szDkrNNBilbmV25uEOk7L3DmhzAQicEdAI", + "W/oheEsy2QFTvcwJX9EFPNKXvhEb3UvYna66J2xuf2KG/3B+Wunz6T7qvWKxTFjnkxTVpjmXYrqiqsXV", + "4b0zI/uGxDSEDftdGhq3pBkjH5X1LZimTCz08tOg17wW4VNe7V6UIzZXcpYzIFeFEdqZMLvKyZInCROG", + "Dmmmqovqa9kH4C+YYP55G9Y5biw0siMBx/lk34zFIjWkKQOUBZHQvqVbTTthuAg7gSG2Zj4UUdK1f6VX", + "wdOCmgijocAVKYTAe8PcfgbkbWHfWbxjB2o0T7JcrjL9BC+PB6n/x0OPlU1YN96B6F3bI+7+s+cPa332", + "FVdLD4tSnN9/9rzdN6Xj4emdkU/lnFw+e8NWK9rfN7en5FMe6u5dqCQkCzBJD/WzBXbs7/8wajV3ISxb", + "fNXMWRP8iu4XVfwhO6uO4ynPd7f2GGtEBQrnLOck5XPDj1rJXRUh251cXCOCjZy0Us5OdsCljaklGHPJ", + "PKWawBGr3Q6HmlVmdlfkVUFrr37oF2ZLgK5lj/KQ7QP6Flgos2m2eaZ3RZyyQhlFBWfMcjmjM55yvX70", + "jDV6j0jTRu0dnSwfrmtuMJuxFUUusCSZEwAkRQ+KpjDA7nQLfXtjb06CWOeOWvUeI1iEF7jqD3nNBDl5", + "ZWmkEYAXfj6PVTvu0XC3lcY8TL83OkK6C4v7bzsDUMY6WRVQ4q5L67ixlngbQk/JZc5YzVcSSDvC2TpM", + "Xk16S5amktzKPE0mvauKq2TQVI3hSWiJDo9GTrI9Pl1t8q4spYNdM8CDDpV2/PuIVJqCGGFEFmwf/Dk2", + "De2/Jr2EajqGr8NMLP6PEVWfP40Gg8Gkd39vVlqVeYOtGz0ffMPAlJwb0mUOq9ulsw5LsjOj8fUtzZOA", + "bO8+xs8zhHbnaFtfis5pAoGtdlgVoU19PX/SB4hIScsdfwud0Wq6UYXPdOosDeCgJ97UiTsgOFXosfXM", + "qV2yOdF5wSLryKdITm87hDAjWhkmVhOpjP4NN3giuFCa0QSOgFHRN6q+kekCcfKDYvMC/HlJRpWynuGl", + "0GXdJ1u9Z3ReiJjqKj+D1deJom2I6zITzLm2cAeEuNOW0bbM00HckEhtIG6tjlGh3Pvx42gw2ts/iPqj", + "wejps+fRaDD64cWPnyLz+/7BU/j92fMfzO8vfvwUeCh1IEHDZekhmf+wKuq7Aw3OkHCR8Jhqcy43NOWJ", + "lQAHE3GWM2VuEmjhbcgGvlZ5wbok2cdymc1aQ3nZ2zCN3IAxUZGP7bi67X7gztQfDr+FkN5ybcPdnn/2", + "rfwaJ/cV97mlk9rbqtTlz/WzaOPWouJxnqN/bc213/1cXSO0dtEuD86Kg7TN+poLrpbnjCozbOPk4Xdy", + "u1yH+orSMsvA2c49J5lfelGvJGtlEE3pQzLnqQaD/7wQ8EAMLVrfnF7bFq/YnKOFurm28pu5iJS4UdHJ", + "K6aCzBgE66AXkbe/NlzRK8M2LTLB32aiWzM6Osnb+RLJKi7UvTcMWzib7S2j4LHJBbjPxgDFNi4q6Iq1", + "q2gi4OXlRiXsr6qtMj2187XqqDSnK6ZZvuGVsI2p/cfF+3fkAgQSgshj4TJztga/qmCKyjtQFegeDO1h", + "F0YpoyJBOhMRNlgMyAUV5HVORcxVLCNydPgg2vtJ2jC/8YMZJNYPSy3OAdcoBWIOzmjYtdw6sq/aOXcx", + "eTj1thU63e1oKXncAsM53N1p7i/vJimyctHvjUiRsLs2vSdhd4hpXJEYZnZvfClXulXdT+XCaNMtrONU", + "VhXtIIqJ7MCbD+pTLImIdZ8URQqaovk/nRncQYRsvvmVMYqbNu6g6EIaG0E7AIhyuKgG180n0xrJdaFz", + "RlfmZgRU00ZCXVwcw3OsJkaZ2m1QIwR5lfNtsz3070dMaXPky5nRyttFIZ60ajyePTYjURASgUNBvKR6", + "YN8KuBQDjCD4tFXElB2tXGTgnm2h8eAhdN2RhKWabgvDV9D4PvpKN6sO6A7EwyVu2uErt4k6B0w19Uom", + "WJcd4jXRqisWFG04bhBcS+flCxS+zwnl3RRSi1Or6jbK6F712Ahaf5YN61EnmLcJem7yqtLitaqG1ZId", + "A0UUfEsQGHE3Q5l4958C6GrsMuqlGLechaJ65QqAjO4H/xOjmd3RdJrvHv++0HxVCOUU6NjyqoBHq1oD", + "NG9YrpDGu2Yuijh4CX90FHEQhb+96hLafOz2ZJfRx3+vWX3sgwc+dxz092Z9rlvfFuCWPsLKk9Fcc/Bc", + "VNZnEW68c6sF3KOKeA6p2g0xX/a2MBqMyE/g4p+vDIprHkfOHeMn9MLIqUjkqha698N27xDmjnhW9ODd", + "KNm1lG2X1rlewGey42RstRvcXaPpmGvpXBa8RGWuuPW6gNekMqMCbsQH6Tp/7q3vdPsbQTa9fiAm9VJm", + "/evyBWbHh0jdaSYUlyJyXoA2CP/w7KTjgenLHnw+94knIASbqVVpj6s/fNfJQgzaqhecbNYKxTYJhu1I", + "Egxi25Idq4Kmaydd721NgGrax2aZshYFJ/gd+GwoTVcZ2cEASLVreaFR8Mq13lJFSsGv9FH6YfR0b//g", + "aTs9bstAcEgKwX8rGOHgmDHnNowN0xBUoFyhd+ZbvMrSPp3Fe/sHbaRuO0NRKey3Rm97mbkpQ1iV2vTx", + "etCkLlNPeoHFpfat1YpSPEY9+tCuHG0pn7vJNt2KD13CldvEtOud8V3dJYDXsajX7tEmV5l+9Kj2Hb+d", + "7GiaTrufQzVN3WAY0Wpf/f8tWGobQatBvbrwqAVEtaW0gd357rTo+OYLgVDVHLRQ8uH8pEFvuuNby05k", + "p/NFsOqx0N6M/+Pl+/Pb0X++WchtosO7XKravJQ6Nh0qS4E71SZprfZc7J5GH7pXHvyecJbKcjnIgwoy", + "fI1ae5QAQA/6bi8D52XcQjN9hENcdz62ZMhn/JhgiPWkt1t95HWB11u7cT/Kp9G/TG5aO6u7W2/3vrnB", + "KbP9w5i/eOTq73RO4S1o0/LfnPJ3x+fEN/ZH8DvLZV8tJTg+el9tEObiQmm5IimdsVTtNuNBDS6vCsH1", + "erhIuWD5FHdysz/Ys8GhjSZ4UJqq68ftslQkNu3Stxqenr7tOqgOneRjl1Ji9pFjAPXwjf32/t27Xx7r", + "OOs846dczOWmAJqNargfBi7kiRmqEUDzimnKU4i3KE2hdGa9W23/MrjCudzGNEPZlTM1mIi3NKu6ZEMo", + "IsbjibmEp27rOiu9ssPw8SIcaiLAayeY2UfH7yBuRcRgoUHCiOQshfWqiFChblmudhvhHolK+WoItx88", + "md8dn2NCi3JSMM3D4L1P9w/jYnfvqOfW1jOHaBfXi3p2dd3Dh7dhq/E/tVqOgpCKTajfPNg63oexGVXE", + "b4NnFP6a0nzhfn5wr4+8FjbOZPPequEnHQTWjdW/2e+vDh67Dht9smkdXUEpTUi7UJbqStPVb4vhPKWi", + "r59Zqq9+K2jS/23Ri6pf4Rz8x0dspS0ZCb6TlaAOWV6FtFaxLYRKm0jgadGx0DZuv5EyblNONTmfK+YS", + "qDEYoy03XBCb2PowZO5Ry+M1jmfak52z4/OIvD9/E5HT90cReXtycVSVHc8AtZvmp1jmrCNhGqZnghZo", + "8dGSNLMx/fjjNjYdiEZ5LKhmbMFF5S6MHpd4zo5jk8mVwPgPuRTkYsVD76FNyZTcGbh9QC66noPeRszp", + "tLla6tgECcokdunYCvMQgGBDdqygY2+mFOl61/nAF+CNRUnYJCJrWYB9C9PMrAkVaw+ZdcbC0aOJgCQH", + "ZphSbjJjOc94ZI850zmG/w0mwgaeVR3wod0T5YOp7EYghls1ch987GUsV6CBgvog1qgzJAWozAnV7HHU", + "7gHLbp2XbM1KOjhJiysf8tFp1zGf2waNI0A38dDk+MQz5Sel2LH20VWgI3ubkIvGtPKH6+pGhydjhx5O", + "OIjI+fHL49PdjWfpRoCA1lKIIRzdRDDdS+NY5zYmOurdyvxaTYE6gEcBS6ZcPO5QO7yzL83PIQQBuTlD", + "tlVdT3nzyS1G62ubLsdozphr50TEA8LBknFUwI0VcvAFHOph5+qAWnRpn25PG9zo7J3GkAKyIwULvALN", + "5O2u41sJ45YBfr47WIcz/gPXZ9N2PWpv2LC7I5VHOUclcSkbrtjGm/IZsMQpPts536PARhTKj/z6W5hL", + "uTdw8KJNSuhIB3jjI/UaEytrhETk3fH5LumXVAGasoSojAqFKXUqQokZzikC4zICgOYzrnOae1ZHwxhh", + "zOlV4XgQWO2Pa0zOWyicy8zpCUHnQZrBrJ4zJsd2hBtIc6FsPlbzEexb9TF2kdhZc9TW2lQLurfpvC2O", + "Cp06b/MYmw8fFeWs8/UjaIWabyd+1AT/pq4XPZLEhIi7lceBv1ANYrlkGIL9SNLWIWgbSdKf5JJnxJoT", + "g7SJnst9toDt3lg8VdsgcT/b6hWV8vTRMKhRHoCjHSqQgTdIvUYB63YzeEgoqyjBLTKZy9jQ7kNc0447", + "gws7AlhzJhKWsyRIy+oECxx5QMCrMuWQx8ClxlfO0u0JkFhMBBUJwQF5mOh1zlmaqKFmqywFt+0Zm5vT", + "j2ma+jhtJpJMctEUpk6ERpHY+q3WY4HQ/P8KclXZn8i8EAk1c9MUksc+SuD6rWD5ui1LOc3jJYGvmKef", + "peyGOmSuO3nXl0lolqU89pRxG19vt5byDDfh39cJFPT42Hmv2+X6ABQsEEtw4REmebEpOBUKKxXB7LEe", + "+h3wsgtsBxMYPTZH86mucL7gVsAwQ51Tocyqq9h6aX42WG7avmH5iooxaal88FUVu3YD1pb2qwfMV9vh", + "qQXeRrh/Lfw0wz0ukhWXoI2yCMe4Q504jVJ0RCpSdITSjtGKzG8zRlcb40836G5fqCFZB74WzixThrEL", + "ziNKAd3FQIE4cCwLQy3WSrMVPLiDi7+vMWLDLlqltHreh/bE362Pow1xbEPm8Hq4bOvp1t5Caym/Nz+D", + "duYHB6+kFo8Q8B/1flO7qDo0facam3Q9HvT9bQaqBHtsHjmYPK1DV+idA/6XEz/tpGe4tPfk2g3O369s", + "W5i1dKjCDTw0u2AHrrkNh9HPhpebz8Gty5fnQ4cnj/VDbca9gF9btwPPlicCu910LJ93EtzfhC2Po8Ph", + "+0TEOUOJKFgvOGNs9v7+3OPxTtTbnRG6EUParlxZZ/Ly4MDfvbXm0sOxKGGdJ+umg/Q46ozs3PKwW5a8", + "/SF3HuDrAODNZfhYqW3C02i+AAm8hSVCVJYNj7dg8q1dZLPfQHhb/pj42CgM7G/EVk16918hQg33t32A", + "WltkVBRA4NMWIN/i7lTWt8392XAG4bgl8IOT2R6MbgcIyy7M3BYLvRdyQ33XuUwVCLIlJ8R0+lKmakAw", + "OxOa0SY9WmhpkASdHRMW84QpclsGwQEQqWVpYZr4SU9Iwcq+4OgtpLY9xBonxKbu3Mvmq0K5ptom1zUS", + "nuljuuB2xzYdPHWlCOJKfKQPAa0VEHPX22yuF8E6ewHufXo4rcR2026kxNUvnx38ucUd6o5//OObCBaf", + "7qPeP1iuuBTdKgMUVoBkdS05O7HognMhrlCQ/dH+0/5or7/37HJvND4YjUej/9t2yRZcT2O5WrUVUXnD", + "wR14xTVZUrWsjI9SxNPWIeX0BrfVMqQk1hOJuDbVqIq9wf6zwaht2M4xnYN824A3e4NR23C1wyq7BvCI", + "QuBXttWGKfUseM5VsMyF91dBu78K2nXjS0dS/7Zg4UWjeBxwZp8ZFFNzqwa+pOyGpV9abOAUBoFTWj8c", + "dPfQaBcwSCt/3m4hlfSd4JcYdQDshuUzgzJrgnAoxdeEzYoFmHag+y3FcnkuHURJTWyDBmnabpeVpaKz", + "IU07lysLnRXaulgTAPaAPHHdsLZeLFOZ898xz7ySKYvIk1+VFE+cpFbEushZApJWRJ6kcjFfafyKBfnY", + "fM5jMHhfs/VPkGmXZJTnKiJPhJSZq+GXMqEHAciC5f+Kfh04NogJMquCLWj8IOg6koc2k2fGMVNqes3W", + "raVvD3++INjEbIycvAqSkl2ztdIyZ0SthaZ3uEMW50yTVMrrIhuQ1xAC6qrRHP58MT08Ojq+uJj+5/F/", + "TU9eESZueC4F2PxvaM7BuY77BM3VUoVrWeR9XEz/mq37vNXk414FWmjsQehz79q5/LxP1MGArujvUtBb", + "NYjl6gmRuTlqVyLrx9FohMf4louT91W3sXpncFEXpzYP4l5rskQDqWkJ/3bgW4CWZ/ClB3BxfHR+fBmc", + "w2ccAk4SnEV7NkilfBDJhqysuEtoi5fJpq3EGhJrEmTQfdTe25YNs/RxRa0xTGyqVPpg1q1jLHBzcXE6", + "vDy9gLkvDgztEFjKSXnVZAzRqNDi8OeLyGZZwnofMU1LVGpJ2/EgJd+y3lHzzmPhmalBa9VW849rltqX", + "btuWmLaQ+3p4coZx8ykX1ySRt5D3X0FedbbK9DoCxycYG+sc2RGMWMQyTbKc31DNiBmHz8kslfH11P44", + "5ZlP0bQ7qL6T2H/a2xUnYlD9Ze/H/cFo8Gi/YweMjOrltsAwbUmWszm/Y65YScrGwyGmkTkw//pwftoA", + "CswRAmVgFErfuVCM0JmSaaGZbWuJ0/CDYrkaJlTT4S52wllMl1kRXzM9xPW4Hqt13/5eZHBAwzo8wzEN", + "uWp0eBwcG+f44C16aXpUqq2UqEFyKiC0fW//B6N5DEbDFxHZGwX//mF/sPcc/trbj4g5/b3nL/Dv5xHZ", + "e/7jYP/ZU/v3bmtwt0Nel8N8akNHKys/aAYW2/zg5ty5SPgNTwqa+qtAZGYDM8E1zo0ZFhMaAXfgKyMD", + "7HUVsPGrU/x3Np2t60kO90ZPXzz74fmos6KN6Wew1g1ky+hgdQGCA1acqv14fnGjB3QNLvTzp27BGDaW", + "8BWGV1dTJIyevuhaJ/QjtzzRy+GS8cUS1pfxOwhLwvyVPm4/Z2Zb1XKcOPgmiDap6f29lVNtxg2K0bJo", + "F+kdAqXtRRiXCOUy1Xg4XHC9LGaG3liBPJkNbRmIZuyNUyOwsJItVgD+Xkj6S+8tqKia+7K+trrz29Oy", + "GNREfPcd+XlJwZ/CDhyWrrR+qMpxldNgdKw46FcQiECHZydgdvv++7KEyBsfVvz992MCD21Q1qnMM7pz", + "dHpyttsIg8OBoIMrLWJGuGArKjSPy5A/WE9YvtjVbO4DwrrKqzierzdixip9BnLWtx4UlvGDowUWqbUr", + "eUdXLHEOeUH1ETOQdWAj6EytIiLzBRX8dxdv5MzVrko5u9PB9s7dezJAyD3qo8qMPpPudd2F1nkvuTJo", + "O7JuIeEvTMcDrGjiSAkkwqKaQamvlFHDIDRx6Ig4OOBymMhYDT0v9vgCuWAgOqsFZ2IqSF5AdkWR0FQK", + "BmXEy0o/VBC8ByROC6VZDshyChhWnl8N0wxhZHea5SDanZ3YWmFMxBAA1m9Bw6shzThGTV6VYnk1Oz+k", + "PHaoVJYOdghwfvimrJNi2oao4rylgF6vzFWBhNHWN4emXK9NlyMmdE5TUAOtCx/Lb8APJ86lUiThhvvN", + "IHm/kAlOdGZYVrzug+csNq/cPohZtiWSUkZvmCJGFjUtcuo1y117ZK8ZJA6xJ/gdabuXiIkYhWswMLxK", + "lSpCrkhzZ+0gHOnw7ASG2e5c3LVEOyvBKrywjpdcGHHdG+Uj0JbdTt6WBMTK8JaQ4IAYMO23CwOazyQo", + "CPcr4IU5/D6yjJIEqYzGzI4EzjfhuiD820eRK7Jz1RlHfrVr7oAR3XAwG6p95IFiBvygmKplW7bWhZ2r", + "z0p0bVNaX1lYmPt6RBXUlyUIGMTWiAAi9hGMOdM5Zzc0jcgNV0YCUXzFU5oDPiPUK+S4jjivS6LrL1Oj", + "rPAu+bcQw4IxyCuLZ2sz2GFn/arOIlQ41hF4vQEt3e9j2VJyeXnqqhNByWxs+dpyBlh7Ra+tF25yLkBz", + "ylN/lzwr2XYP4cVq2UhQGAtH/HthyNPvnnkeVgu8Gqz5DZuUtRr5vAR1gL+me8XXEkiedekjO9a3cklF", + "khoFhLM0Cfy60eE65TGzTyFOqklTcm7kK0XOGZZxb4g4JU9J2YJi8h2usRCEZRyHZye94Bmhd7NH02xJ", + "9yDvCGqivXHvYDAaHPSinter8ORBG5WqzTyTpVzb+iMtFf0ta1WOCVSlhlpCAScyvbVIixgACF8WPmvi", + "OibWN0KlNeuhJKbMynRZlvyG5SnNTOMPrtbUTz5hgfn5NVUoNlUyP7llAF699depKSH5moOOyIQXu0/e", + "bqL1gfNe9aY9+sYQezPNX+coWYHqCjcyLO9qaPO+qzg8gG4MrIU+m8NM6qU1tANYdOC/5Y7KFtU2f11d", + "Xf2qDKP7YyJIpaKAr1SKJZmdvzAQ0Agb4zRIYgUh5ifYGg5gDylynzTNF8wlPzFNno1G/qM96MpX/1Gx", + "jEKwLA48mQjzX898vp+Ie9gFvgM7dfAkgXeXQlxf4iOHVX1fymRdS/wXeP4Of7VZIfEp4OFMbkXpWX5f", + "fWIxSjj8gMwbbuX+aPS157ZPsDB5m98eoGVCVAHmw3mRpmBAePoVV4LprVtWcCIw13vuIBT1nv0581qR", + "2Op6zDaMeqpYrWi+dqjRQv8UW6A7iGk+xOIB3WTUJUAK9RGjRpS1iFz1ISAYWaUkVVmEx1UOsjxnYIt0", + "YmlvtbFEFT6p2PBcL6Jx8SsaRyGgcJ7K2xYq3Ufhqlxrhf22OiZfoZvlkt6UdZXKk0FdAC6Acp8HUog7", + "S57hhz4XXOOvc5466Zt4OWwiQuVWCs1F4bl8Fd5x8FU5CAME64r2ibAcFDXbWArUBnAc6FlVf77//k3B", + "E5bUVPTG/JhKxDIQFySNcMG3MDvaUSBLbhoSlnLDwT4friik2mQHn9thq7ttNDygwa01rjz9xjEcuW8r", + "PWWIrKOvLYsI6lh90UKCvfbG5OPH0WAvIqMBvsJ/isjH0eDA/PAUf/i0gehjYaZvSPWrFfL+ZLJfKzvV", + "Rffxsv33oftPR0+//by2aKjUBGLfIJ2ZZFh/2npcl4W2/snMyMx+8O1nR3Rx3hWkCAqf7wgZlsmzgaw1", + "Nmn7t5FJy+48+0COidEBDzJMZSuphOOhiT8szoUzpDUjWMAfrfmKq3AnaPMEC8sTVTWsWNOBK9UXYUwc", + "JClNIGsA9m3kjAsssg6RzBiB0aTJXt+WZXEf1B7CGtNl1V1bfdUIFLgL7BHY7bSE1P2lVhWuxnHRPrib", + "rqwpwr1ZtedB2x2TwxheDsOaWlaJL4FQHwyMS9WuBrD2OeGG09IiZOFUsRxhWfKg6hpAzQdJfNMabG2l", + "jbvrsm1Vig1NTN+mEFtLRsPdL1QhgzrY8PYfg73LPhSaERKWFEiZWDIR1jQAxzFP4QUJa0bfYBXrpBAJ", + "FRokT3cr6iZM87P3LvDRXD7ICLKPGKyxCLVkNLEW8auQSMpYM91H5+4rbxRVLOc0tbYhbyKNyEoqTbzv", + "0G5jNENyr8bo4O34NxKEIBkb0IsOwQzx2GBdmICygV3jjXJSV+FqKyhhSTQjI9Vw3yBVPah10vsUSm9v", + "WwsON/Fp8wI3VbVuWyao8A/eGUqypdQSg+Ziqs0Faun6ZbfIZiW1l8kMv0mQdLyqNJR/I4GyUsbyT5Yn", + "q1XmzNxdF6w6Zs27AG5e3928tpJpYWLuGTRv8VFtyC8l7MP6o//CQu1/KyOK112DinogBfr6C1sIgoHp", + "BCW9IImoFS9AwiQHu4OJeG0OvSHtGRHJJpY/WlJNjnxaZWUrP36BcBZkLv2K0tl33xFfTAjYIdPkCm8a", + "+rFdYTg7lsW/gEPoXxj2eAwVFKDe0C5un5eRShOBbn1YgWFAjmm8tFWJwPOCC0UouTJQKoEExrArG3cD", + "0jWzIVPoZA+T4EMe+fjq/bvjT1eDFjnO/AQmpY5c+Fb6Rpbq6mmAT9N4G9NBJUOs5zRuoCqzyWVqOQZG", + "VSO7sDcGP/yXLPD8yZKl2bxIiQ+4HlS4TzlWoVjeMhLkD/hfkx5yFLcuX77ENNp/9tz+jnA1v4F7Ypd1", + "5Z0UfR+15mwrM6qWEyOtpaT/Czl7f3EJ7gnj4bD0nH0xejECdPSV1ScTM3P/b2TSsz6L/ct1xsakTjUm", + "Pdc2IU/aDPobDqJxFNsAzjnZ1OWWf5/07j+Vw1YguTcagdn/SSvYLv6ng8zjWgmfEqMM2agAp1OmOfJh", + "It9CoKnX9vmTZZpGsY57GzgzBELYKs9sXYitjVcedVT6GJBzW/4YlAqjeInwXns1K5oImbcR+WrNLK+V", + "7YSMAhK//SUG/U80370JSrN2mPCspFRW/NntEtICQcuIWC2KHwpvNtvz+I/egum2bC2I0ka0UaCvNbP1", + "R/61Oyp9wqIyUXxE3h2fRwQTUlWyOvsXamcRQ0tW0z2k3Xpm2v7d+31AgIW5RvBgddXnL66IKuZzfudM", + "JvbVHic57KqLQHZmBU91nwuwGpylhYIg542rCj0CrBHE+rBssaWav8vxKtNrDBNpHrgd/o0XU3H8ei7+", + "LpF22/HLnHjbbaDMVQtTnDioVlMFGwLXWgzBz2rfHnHOmrPnpqdK0/zyWURenx6+65t/vDw8v0ScO337", + "9zekxU/U+11VFIRANzDSv72HVguA8dDENmjht6dc6bcue/o3Y3i1KiFd5FM5N7Z/lg79kv73dAY4bdPS", + "kBh6PO5WZf3FUGAiTIIMnw/6PFs/5NL12RJouFthaZAadURPayOToi0V3IrzhBzJd6enlUTa1un87Pgc", + "/NxgQbBOr11PggTEk571i3x//gYcVcN1lx0wOzGKp2V+Yt/59P2R6XzqPbt9x3fslvyXzK+xK+QlYb7b", + "25ML6PeWq5ilKRVMFspDs8V/4TFafIMgefvzb01+sQMdp/wFOCrA8o5qJoe/FRDy+prGjLw8Pr+07mvN", + "MzOdqzTPPSHFbVnVbzhFmwL+fVWWnn7YdyvQO1pSgnuVGTKaWRvyhtzT9eNtpJ+umJcbpK+ax5Ozb2U9", + "bWS1/5O1jWae7DY7piMJ1nH0Lyvmv4L4HkTCfL787vGrxl0cfzKC4ibm1Lc5P7NOF2fvAlumTNWSUBs0", + "U2FI1Yowjh0doYv0ucs960sQoSnRelCvCqXHE7E38CFBdj6XghaJtvMzVROxbxRoyJsIKYVcgtqJOBiQ", + "CyaSlj3Z915CFbmy+7vCdXBFEqb4QpBrxjJVZp7QVLOUKQWiHMSKKrtkiJNEKm3gX+bOTeWCx4Mv5kgV", + "Gb+FHzkrOPaa+g/AlTCCoer3nuVszvJWllZ1fn8EJ+lIZey5CWAJNt2YVLfmdBYabCeNLMJv7UindqQx", + "gQNcFDxhBEMGSj9kMwAkGHatyesgwfCYvGNFTlMimLYsLmfQufYAOBGvwWMI0c/zc4fywPXr3v8RBAVr", + "j+R9xRM2EVcpnw19V8PD42uIu1zyeOlDCUp8ejjjcp23mqHPbNbhb8VXw7zZfzpTrSRNbqGtdvP2gP7i", + "p/8a/NRdxi/gpmaIkvWtS6bnGCpYEDZx1FqG6U3BqC0GCFD4vpx3VOwctWnB3GX9kIFXRE3v6yh0yq74", + "Yzd1nS3sJW0u3H93PTo8nt2IlvFjMYo+oJCRTijPlR8oX3eNosrE7668TihDgPJLc5otc6p8bDNAz7n/", + "k4TPIYQUnJYSF11tU4W76Owyc7jp5ap5pFQsCnjBafBUw03agFaCFMG3+fFzQ/rvqreN0+gWdfCOyaQY", + "jQ7iZQr/Z+QlW0sj5HX8rDTNwdilyLHWlPwHGAuM5kcTnqY0BlNgnqgHFUCA8jf0xK7ljP/TeVQ1c3qX", + "L3bu05v/xaL+erHZikcF5KmNryCnCnJLbnyoqaWajMjCp8iMyMyn40SG0Mx1OWh5RNb/8Lknv9n1qmcZ", + "bQG2bRImnPzn2LJrD27awa66MmgGSKkgVW17CpPDsxOLuj5eeEgz3rv/dP//AwAA//+Yj1XW28UAAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/pkg/termite/lib/seq2seq/t5gemma2_decoder.go b/pkg/termite/lib/seq2seq/t5gemma2_decoder.go index c2c44a7..cdb392c 100644 --- a/pkg/termite/lib/seq2seq/t5gemma2_decoder.go +++ b/pkg/termite/lib/seq2seq/t5gemma2_decoder.go @@ -83,6 +83,15 @@ func (g *T5Gemma2Generator) runDecoderWithEmbeddings( return nil, fmt.Errorf("creating encoder_hidden_states tensor: %w", err) } + // Track whether tensors have been transferred to batch ownership. + // If any operation fails before batch takes ownership, defers will clean up. + tensorsOwnedByBatch := false + defer func() { + if !tensorsOwnedByBatch { + encoderHiddenStatesTensor.Destroy() + } + }() + // Create attention mask var attentionMask []int64 if input.AttentionMask != nil && len(input.AttentionMask) > 0 { @@ -104,9 +113,13 @@ func (g *T5Gemma2Generator) runDecoderWithEmbeddings( flatAttentionMask, ) if err != nil { - encoderHiddenStatesTensor.Destroy() return nil, fmt.Errorf("creating encoder_attention_mask tensor: %w", err) } + defer func() { + if !tensorsOwnedByBatch { + encoderAttentionMaskTensor.Destroy() + } + }() // Create a Seq2SeqBatch and set encoder outputs directly batch := pipelines.NewSeq2SeqBatch(batchSize) @@ -114,7 +127,7 @@ func (g *T5Gemma2Generator) runDecoderWithEmbeddings( batch.SetEncoderHiddenStates(encoderHiddenStatesTensor) batch.SetEncoderAttentionMask(encoderAttentionMaskTensor) - // Set cleanup function for encoder tensors + // Set cleanup function for encoder tensors - batch now owns the tensors batch.SetDestroyEncoder(func() error { err1 := encoderHiddenStatesTensor.Destroy() err2 := encoderAttentionMaskTensor.Destroy() @@ -123,6 +136,7 @@ func (g *T5Gemma2Generator) runDecoderWithEmbeddings( } return err2 }) + tensorsOwnedByBatch = true // Tensors are now managed by batch.Destroy() // Ensure cleanup on exit defer func() { From 4ceccc28864ec930b774809b856d23bac490f37d Mon Sep 17 00:00:00 2001 From: timkaye11 Date: Mon, 5 Jan 2026 22:51:21 -0800 Subject: [PATCH 6/7] cleanup --- docs/T5GEMMA2.md | 23 +- e2e/t5gemma2_test.go | 155 ----- openapi.yaml | 142 ----- pkg/client/client.go | 94 --- pkg/client/oapi/client.gen.go | 614 +++++++------------- pkg/termite/api.gen.go | 461 +++++++-------- pkg/termite/api.go | 156 ----- pkg/termite/lib/seq2seq/decoder.go | 92 --- pkg/termite/lib/seq2seq/t5gemma2.go | 183 ++---- pkg/termite/lib/seq2seq/t5gemma2_decoder.go | 201 ------- pkg/termite/lib/seq2seq/t5gemma2_stub.go | 15 - pkg/termite/openapi.yaml | 146 ----- scripts/export_t5gemma2.py | 25 +- 13 files changed, 461 insertions(+), 1846 deletions(-) delete mode 100644 pkg/termite/lib/seq2seq/decoder.go delete mode 100644 pkg/termite/lib/seq2seq/t5gemma2_decoder.go diff --git a/docs/T5GEMMA2.md b/docs/T5GEMMA2.md index 1b6fe6d..f026791 100644 --- a/docs/T5GEMMA2.md +++ b/docs/T5GEMMA2.md @@ -65,15 +65,11 @@ huggingface-cli login cd termite # Export the 270M model (recommended for development, ~6.7GB) -./scripts/export_t5gemma2.py \ - --model google/t5gemma-2-270m-270m \ - --output ~/.termite/models/rewriters/google/t5gemma-2-270m - -# Verify the export with a test -./scripts/export_t5gemma2.py \ - --model google/t5gemma-2-270m-270m \ - --output ~/.termite/models/rewriters/google/t5gemma-2-270m \ - --test +# Uses default output: ~/.termite/models/rewriters/google/t5gemma-2-270m +./scripts/export_t5gemma2.py --model google/t5gemma-2-270m-270m + +# Or export with a test to verify the output +./scripts/export_t5gemma2.py --model google/t5gemma-2-270m-270m --test ``` ### Available Models @@ -258,10 +254,11 @@ The 270M model requires significant RAM for inference due to ONNX model sizes: - **With vision encoder**: Add ~2GB RAM (vision_encoder.onnx: 1.67GB) **Memory reduction tips:** -1. **Use text-only mode**: If you don't need images, the vision encoder loads lazily -2. **Limit max_loaded_models**: Set `max_loaded_models: 1` in config -3. **Reduce keep_alive**: Set `keep_alive: 1m` to unload models quickly -4. **Test on a machine with more RAM**: The 270M model needs at least 8GB free RAM +1. **Limit max_loaded_models**: Set `max_loaded_models: 1` in config +2. **Reduce keep_alive**: Set `keep_alive: 1m` to unload models quickly +3. **Test on a machine with more RAM**: The 270M model needs at least 8GB free RAM + +**Note:** The vision encoder is loaded eagerly at initialization time as it is required for proper text generation behavior. ### Slow Inference diff --git a/e2e/t5gemma2_test.go b/e2e/t5gemma2_test.go index efdf9ce..4b9149a 100644 --- a/e2e/t5gemma2_test.go +++ b/e2e/t5gemma2_test.go @@ -369,161 +369,6 @@ func testT5Gemma2RewriteMultiple(t *testing.T, ctx context.Context, c *client.Te } } -// TestT5Gemma2DecodeE2E tests the T5Gemma-2 decode endpoint for embedding-to-text generation. -func TestT5Gemma2DecodeE2E(t *testing.T) { - if testing.Short() { - t.Skip("Skipping E2E test in short mode") - } - - // Check if model exists in rewriters directory (seq2seq models) - modelsDir := getT5Gemma2ModelsDir(t) - if modelsDir == "" { - t.Skip("No models directory available") - } - - rewritersDir := filepath.Join(modelsDir, "rewriters") - modelPath := filepath.Join(rewritersDir, "google", "t5gemma-2-270m") - if !fileExists(filepath.Join(modelPath, "encoder.onnx")) { - t.Skipf("T5Gemma-2 generator model not found at %s. Skipping E2E test.", modelPath) - } - - t.Logf("Using models directory: %s", modelsDir) - - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) - defer cancel() - - logger := zaptest.NewLogger(t) - port := findAvailablePort(t) - serverURL := "http://localhost:" + itoa(port) - - config := termite.Config{ - ApiUrl: serverURL, - ModelsDir: modelsDir, - } - - serverCtx, serverCancel := context.WithCancel(ctx) - defer serverCancel() - - readyC := make(chan struct{}) - serverDone := make(chan struct{}) - - go func() { - defer close(serverDone) - termite.RunAsTermite(serverCtx, logger, config, readyC) - }() - - select { - case <-readyC: - t.Log("Server is ready") - case <-time.After(120 * time.Second): - t.Fatal("Timeout waiting for server to be ready") - } - - termiteClient, err := client.NewTermiteClient(serverURL, nil) - require.NoError(t, err, "Creating client failed") - - t.Run("DecodeFromPrompt", func(t *testing.T) { - testT5Gemma2DecodeFromPrompt(t, ctx, termiteClient) - }) - - t.Run("DecodeFromEmbeddings", func(t *testing.T) { - testT5Gemma2DecodeFromEmbeddings(t, ctx, termiteClient) - }) - - t.Run("DecodeValidation", func(t *testing.T) { - testT5Gemma2DecodeValidation(t, ctx, termiteClient) - }) - - serverCancel() - select { - case <-serverDone: - t.Log("Server shutdown complete") - case <-time.After(30 * time.Second): - t.Log("Server shutdown timeout") - } -} - -func testT5Gemma2DecodeFromPrompt(t *testing.T, ctx context.Context, c *client.TermiteClient) { - t.Helper() - - prompt := "The T5Gemma-2 model is a powerful encoder-decoder architecture." - - resp, err := c.DecodeText(ctx, t5Gemma2ModelName, prompt, nil) - require.NoError(t, err, "T5Gemma-2 decode from prompt failed") - require.NotNil(t, resp, "Response should not be nil") - require.Equal(t, t5Gemma2ModelName, resp.Model, "Response model should match") - require.NotEmpty(t, resp.Texts, "Should return generated text") - - t.Logf("Prompt: %q", prompt) - t.Logf("Generated: %q", resp.Texts[0]) -} - -func testT5Gemma2DecodeFromEmbeddings(t *testing.T, ctx context.Context, c *client.TermiteClient) { - t.Helper() - - // First, get embeddings from the embedder endpoint - // T5Gemma-2 returns token-level embeddings (seq_len * hidden_size) - inputs := []string{"Hello, this is a test input for embedding-to-text generation."} - - embeddings, err := c.Embed(ctx, t5Gemma2ModelName, inputs) - require.NoError(t, err, "Getting embeddings failed") - require.Len(t, embeddings, 1, "Should have one embedding") - - // The embedding is flattened: seq_len * hidden_size - // We need to reshape it to [seq_len, hidden_size] for the decode endpoint - flatEmbedding := embeddings[0] - require.NotEmpty(t, flatEmbedding, "Embedding should not be empty") - - // T5Gemma-2 hidden size is 640 - seqLen := len(flatEmbedding) / t5Gemma2HiddenSize - require.Greater(t, seqLen, 0, "Should have at least one token embedding") - t.Logf("Embedding has %d tokens with hidden size %d", seqLen, t5Gemma2HiddenSize) - - // Reshape to [seq_len, hidden_size] - reshapedEmbeddings := make([][]float32, seqLen) - for i := 0; i < seqLen; i++ { - start := i * t5Gemma2HiddenSize - end := start + t5Gemma2HiddenSize - reshapedEmbeddings[i] = flatEmbedding[start:end] - } - - // Try to decode from embeddings - // Note: This may return an error if the decoder isn't fully implemented yet - resp, err := c.DecodeFromEmbeddings(ctx, t5Gemma2ModelName, reshapedEmbeddings, nil) - if err != nil { - // Expected for now - the decoder implementation returns a placeholder error - t.Logf("DecodeFromEmbeddings returned expected error (feature not fully implemented): %v", err) - return - } - - require.NotNil(t, resp, "Response should not be nil") - require.Equal(t, t5Gemma2ModelName, resp.Model, "Response model should match") - require.NotEmpty(t, resp.Texts, "Should return generated text") - - t.Logf("Input: %q", inputs[0]) - t.Logf("Decoded from embeddings: %q", resp.Texts[0]) -} - -func testT5Gemma2DecodeValidation(t *testing.T, ctx context.Context, c *client.TermiteClient) { - t.Helper() - - // Test that we get proper errors for invalid requests - - // Test: no prompt or embeddings - prompt := "" - _, err := c.DecodeText(ctx, t5Gemma2ModelName, prompt, nil) - require.Error(t, err, "Should fail when prompt is empty") - assert.Contains(t, err.Error(), "bad request", "Should be a bad request error") - - // Test: wrong embedding dimensions - invalidEmbeddings := [][]float32{ - {1.0, 2.0, 3.0}, // Wrong size - should be 640 - } - _, err = c.DecodeFromEmbeddings(ctx, t5Gemma2ModelName, invalidEmbeddings, nil) - require.Error(t, err, "Should fail with wrong embedding dimensions") - t.Logf("Invalid dimensions error: %v", err) -} - // Helper functions func fileExists(path string) bool { _, err := os.Stat(path) diff --git a/openapi.yaml b/openapi.yaml index 37a5f72..22f92c1 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -518,80 +518,6 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' - /decode: - post: - summary: Decode text from embeddings using T5Gemma-2 - description: | - Generate text using the T5Gemma-2 decoder from pre-computed embeddings or text prompts. - This enables embedding-to-text generation for custom embedding injection workflows. - - ## Models - - - Uses T5Gemma-2 models from `models_dir/rewriters/` that have decoder components - - Requires decoder.onnx and decoder-init.onnx files - - ## Use Cases - - - **Text Continuation**: Generate text continuations from prompts - - **Embedding Inversion**: Reconstruct text from embeddings - - **Guided Generation**: Generate text conditioned on custom encoder outputs - - **Cross-modal Generation**: Generate text from vision embeddings - - ## Example (with prompt) - - ```json - { - "model": "google/t5gemma-2-270m", - "prompt": "The meaning of life is" - } - ``` - - ## Example (with embeddings) - - ```json - { - "model": "google/t5gemma-2-270m", - "embeddings": [[0.1, 0.2, ...], [0.3, 0.4, ...]] - } - ``` - operationId: decodeText - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/DecodeRequest' - responses: - '200': - description: Text decoded successfully - content: - application/json: - schema: - $ref: '#/components/schemas/DecodeResponse' - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - '404': - description: Model not found or does not support decoding - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - '500': - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - '503': - description: Decode service unavailable (no compatible models) - content: - application/json: - schema: - $ref: '#/components/schemas/Error' /models: get: summary: List available models @@ -1109,74 +1035,6 @@ components: items: type: string description: Rewritten texts (array of arrays, one per input, multiple per beam) - DecodeRequest: - type: object - required: - - model - properties: - model: - type: string - description: Name of T5Gemma-2 or compatible decoder model - example: google/t5gemma-2-270m - prompt: - type: string - description: Input prompt for text generation (mutually exclusive with embeddings) - example: The meaning of life is - embeddings: - type: array - items: - type: array - items: - type: number - format: float - description: | - Pre-computed encoder hidden states for embedding-to-text generation. - Shape: [sequence_length, hidden_size] for single input. - When provided, the decoder generates text directly from these embeddings - without running the encoder. Mutually exclusive with 'prompt'. - sequence_length: - type: integer - description: The sequence length of the embeddings (for reshaping flat arrays) - attention_mask: - type: array - items: - type: integer - description: Optional attention mask for embeddings. Shape [sequence_length]. - max_tokens: - type: integer - description: Maximum number of tokens to generate (default 256) - default: 256 - temperature: - type: number - format: float - description: Sampling temperature (default 1.0) - default: 1 - top_p: - type: number - format: float - description: Nucleus sampling probability (default 1.0) - default: 1 - DecodeResponse: - type: object - required: - - model - - texts - properties: - model: - type: string - description: Name of model used for decoding - texts: - type: array - items: - type: string - description: Generated text sequences - tokens: - type: array - items: - type: array - items: - type: integer - description: Token IDs for each generated sequence (optional) ModelsResponse: type: object required: diff --git a/pkg/client/client.go b/pkg/client/client.go index 8cbd5f0..b1bf1d6 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -421,100 +421,6 @@ func (c *TermiteClient) RewriteText(ctx context.Context, model string, inputs [] return resp.JSON200, nil } -// DecodeConfig contains configuration for text decoding/generation. -type DecodeConfig struct { - MaxTokens int - Temperature float32 - TopP float32 -} - -// DecodeText decodes text from a prompt using a Seq2Seq model. -func (c *TermiteClient) DecodeText(ctx context.Context, model string, prompt string, cfg *DecodeConfig) (*oapi.DecodeResponse, error) { - req := oapi.DecodeRequest{ - Model: model, - Prompt: prompt, - } - - if cfg != nil { - if cfg.MaxTokens > 0 { - req.MaxTokens = cfg.MaxTokens - } - if cfg.Temperature > 0 { - req.Temperature = cfg.Temperature - } - if cfg.TopP > 0 { - req.TopP = cfg.TopP - } - } - - resp, err := c.client.DecodeTextWithResponse(ctx, req) - if err != nil { - return nil, fmt.Errorf("sending request: %w", err) - } - - if resp.JSON400 != nil { - return nil, fmt.Errorf("bad request: %s", resp.JSON400.Error) - } - if resp.JSON404 != nil { - return nil, fmt.Errorf("model not found: %s", resp.JSON404.Error) - } - if resp.JSON500 != nil { - return nil, fmt.Errorf("server error: %s", resp.JSON500.Error) - } - if resp.JSON503 != nil { - return nil, fmt.Errorf("service unavailable: %s", resp.JSON503.Error) - } - if resp.JSON200 == nil { - return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode(), string(resp.Body)) - } - - return resp.JSON200, nil -} - -// DecodeFromEmbeddings generates text from pre-computed encoder hidden states. -// This enables embedding-to-text generation by bypassing the encoder. -func (c *TermiteClient) DecodeFromEmbeddings(ctx context.Context, model string, embeddings [][]float32, cfg *DecodeConfig) (*oapi.DecodeResponse, error) { - req := oapi.DecodeRequest{ - Model: model, - Embeddings: embeddings, - } - - if cfg != nil { - if cfg.MaxTokens > 0 { - req.MaxTokens = cfg.MaxTokens - } - if cfg.Temperature > 0 { - req.Temperature = cfg.Temperature - } - if cfg.TopP > 0 { - req.TopP = cfg.TopP - } - } - - resp, err := c.client.DecodeTextWithResponse(ctx, req) - if err != nil { - return nil, fmt.Errorf("sending request: %w", err) - } - - if resp.JSON400 != nil { - return nil, fmt.Errorf("bad request: %s", resp.JSON400.Error) - } - if resp.JSON404 != nil { - return nil, fmt.Errorf("model not found: %s", resp.JSON404.Error) - } - if resp.JSON500 != nil { - return nil, fmt.Errorf("server error: %s", resp.JSON500.Error) - } - if resp.JSON503 != nil { - return nil, fmt.Errorf("service unavailable: %s", resp.JSON503.Error) - } - if resp.JSON200 == nil { - return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode(), string(resp.Body)) - } - - return resp.JSON200, nil -} - // GenerateConfig contains configuration for text generation. type GenerateConfig struct { MaxTokens int diff --git a/pkg/client/oapi/client.gen.go b/pkg/client/oapi/client.gen.go index 36dd4e7..dacef44 100644 --- a/pkg/client/oapi/client.gen.go +++ b/pkg/client/oapi/client.gen.go @@ -292,48 +292,6 @@ type ContentPart struct { union json.RawMessage } -// DecodeRequest defines model for DecodeRequest. -type DecodeRequest struct { - // AttentionMask Optional attention mask for embeddings. Shape [sequence_length]. - AttentionMask []int `json:"attention_mask,omitempty,omitzero"` - - // Embeddings Pre-computed encoder hidden states for embedding-to-text generation. - // Shape: [sequence_length, hidden_size] for single input. - // When provided, the decoder generates text directly from these embeddings - // without running the encoder. Mutually exclusive with 'prompt'. - Embeddings [][]float32 `json:"embeddings,omitempty,omitzero"` - - // MaxTokens Maximum number of tokens to generate (default 256) - MaxTokens int `json:"max_tokens,omitempty,omitzero"` - - // Model Name of T5Gemma-2 or compatible decoder model - Model string `json:"model"` - - // Prompt Input prompt for text generation (mutually exclusive with embeddings) - Prompt string `json:"prompt,omitempty,omitzero"` - - // SequenceLength The sequence length of the embeddings (for reshaping flat arrays) - SequenceLength int `json:"sequence_length,omitempty,omitzero"` - - // Temperature Sampling temperature (default 1.0) - Temperature float32 `json:"temperature,omitempty,omitzero"` - - // TopP Nucleus sampling probability (default 1.0) - TopP float32 `json:"top_p,omitempty,omitzero"` -} - -// DecodeResponse defines model for DecodeResponse. -type DecodeResponse struct { - // Model Name of model used for decoding - Model string `json:"model"` - - // Texts Generated text sequences - Texts []string `json:"texts"` - - // Tokens Token IDs for each generated sequence (optional) - Tokens [][]int `json:"tokens,omitempty,omitzero"` -} - // EmbedRequest defines model for EmbedRequest. type EmbedRequest struct { // Input Input content to embed. Supports three formats: @@ -819,9 +777,6 @@ type VersionResponse struct { // ChunkTextJSONRequestBody defines body for ChunkText for application/json ContentType. type ChunkTextJSONRequestBody = ChunkRequest -// DecodeTextJSONRequestBody defines body for DecodeText for application/json ContentType. -type DecodeTextJSONRequestBody = DecodeRequest - // GenerateEmbeddingsJSONRequestBody defines body for GenerateEmbeddings for application/json ContentType. type GenerateEmbeddingsJSONRequestBody = EmbedRequest @@ -1189,11 +1144,6 @@ type ClientInterface interface { ChunkText(ctx context.Context, body ChunkTextJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - // DecodeTextWithBody request with any body - DecodeTextWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - DecodeText(ctx context.Context, body DecodeTextJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - // GenerateEmbeddingsWithBody request with any body GenerateEmbeddingsWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1250,30 +1200,6 @@ func (c *Client) ChunkText(ctx context.Context, body ChunkTextJSONRequestBody, r return c.Client.Do(req) } -func (c *Client) DecodeTextWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewDecodeTextRequestWithBody(c.Server, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) DecodeText(ctx context.Context, body DecodeTextJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewDecodeTextRequest(c.Server, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - func (c *Client) GenerateEmbeddingsWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewGenerateEmbeddingsRequestWithBody(c.Server, contentType, body) if err != nil { @@ -1458,46 +1384,6 @@ func NewChunkTextRequestWithBody(server string, contentType string, body io.Read return req, nil } -// NewDecodeTextRequest calls the generic DecodeText builder with application/json body -func NewDecodeTextRequest(server string, body DecodeTextJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewDecodeTextRequestWithBody(server, "application/json", bodyReader) -} - -// NewDecodeTextRequestWithBody generates requests for DecodeText with any type of body -func NewDecodeTextRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/decode") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - // NewGenerateEmbeddingsRequest calls the generic GenerateEmbeddings builder with application/json body func NewGenerateEmbeddingsRequest(server string, body GenerateEmbeddingsJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -1800,11 +1686,6 @@ type ClientWithResponsesInterface interface { ChunkTextWithResponse(ctx context.Context, body ChunkTextJSONRequestBody, reqEditors ...RequestEditorFn) (*ChunkTextResponse, error) - // DecodeTextWithBodyWithResponse request with any body - DecodeTextWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*DecodeTextResponse, error) - - DecodeTextWithResponse(ctx context.Context, body DecodeTextJSONRequestBody, reqEditors ...RequestEditorFn) (*DecodeTextResponse, error) - // GenerateEmbeddingsWithBodyWithResponse request with any body GenerateEmbeddingsWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*GenerateEmbeddingsResponse, error) @@ -1861,32 +1742,6 @@ func (r ChunkTextResponse) StatusCode() int { return 0 } -type DecodeTextResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *DecodeResponse - JSON400 *Error - JSON404 *Error - JSON500 *Error - JSON503 *Error -} - -// Status returns HTTPResponse.Status -func (r DecodeTextResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r DecodeTextResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - type GenerateEmbeddingsResponse struct { Body []byte HTTPResponse *http.Response @@ -2080,23 +1935,6 @@ func (c *ClientWithResponses) ChunkTextWithResponse(ctx context.Context, body Ch return ParseChunkTextResponse(rsp) } -// DecodeTextWithBodyWithResponse request with arbitrary body returning *DecodeTextResponse -func (c *ClientWithResponses) DecodeTextWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*DecodeTextResponse, error) { - rsp, err := c.DecodeTextWithBody(ctx, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseDecodeTextResponse(rsp) -} - -func (c *ClientWithResponses) DecodeTextWithResponse(ctx context.Context, body DecodeTextJSONRequestBody, reqEditors ...RequestEditorFn) (*DecodeTextResponse, error) { - rsp, err := c.DecodeText(ctx, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseDecodeTextResponse(rsp) -} - // GenerateEmbeddingsWithBodyWithResponse request with arbitrary body returning *GenerateEmbeddingsResponse func (c *ClientWithResponses) GenerateEmbeddingsWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*GenerateEmbeddingsResponse, error) { rsp, err := c.GenerateEmbeddingsWithBody(ctx, contentType, body, reqEditors...) @@ -2240,60 +2078,6 @@ func ParseChunkTextResponse(rsp *http.Response) (*ChunkTextResponse, error) { return response, nil } -// ParseDecodeTextResponse parses an HTTP response from a DecodeTextWithResponse call -func ParseDecodeTextResponse(rsp *http.Response) (*DecodeTextResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &DecodeTextResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest DecodeResponse - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: - var dest Error - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON400 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: - var dest Error - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON404 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: - var dest Error - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON500 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 503: - var dest Error - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON503 = &dest - - } - - return response, nil -} - // ParseGenerateEmbeddingsResponse parses an HTTP response from a GenerateEmbeddingsWithResponse call func ParseGenerateEmbeddingsResponse(rsp *http.Response) (*GenerateEmbeddingsResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -2639,210 +2423,200 @@ func ParseGetVersionResponse(rsp *http.Response) (*GetVersionResponse, error) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "cx7df7r/vwEAAP//LcZSyv23AAA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/pkg/termite/api.gen.go b/pkg/termite/api.gen.go index a93340d..b52bf1c 100644 --- a/pkg/termite/api.gen.go +++ b/pkg/termite/api.gen.go @@ -292,48 +292,6 @@ type ContentPart struct { union json.RawMessage } -// DecodeRequest defines model for DecodeRequest. -type DecodeRequest struct { - // AttentionMask Optional attention mask for embeddings. Shape [sequence_length]. - AttentionMask []int `json:"attention_mask,omitempty,omitzero"` - - // Embeddings Pre-computed encoder hidden states for embedding-to-text generation. - // Shape: [sequence_length, hidden_size] for single input. - // When provided, the decoder generates text directly from these embeddings - // without running the encoder. Mutually exclusive with 'prompt'. - Embeddings [][]float32 `json:"embeddings,omitempty,omitzero"` - - // MaxTokens Maximum number of tokens to generate (default 256) - MaxTokens int `json:"max_tokens,omitempty,omitzero"` - - // Model Name of T5Gemma-2 or compatible decoder model - Model string `json:"model"` - - // Prompt Input prompt for text generation (mutually exclusive with embeddings) - Prompt string `json:"prompt,omitempty,omitzero"` - - // SequenceLength The sequence length of the embeddings (for reshaping flat arrays) - SequenceLength int `json:"sequence_length,omitempty,omitzero"` - - // Temperature Sampling temperature (default 1.0) - Temperature float32 `json:"temperature,omitempty,omitzero"` - - // TopP Nucleus sampling probability (default 1.0) - TopP float32 `json:"top_p,omitempty,omitzero"` -} - -// DecodeResponse defines model for DecodeResponse. -type DecodeResponse struct { - // Model Name of model used for decoding - Model string `json:"model"` - - // Texts Generated text sequences - Texts []string `json:"texts"` - - // Tokens Token IDs for each generated sequence (optional) - Tokens [][]int `json:"tokens,omitempty,omitzero"` -} - // EmbedRequest defines model for EmbedRequest. type EmbedRequest struct { // Input Input content to embed. Supports three formats: @@ -819,9 +777,6 @@ type VersionResponse struct { // ChunkTextJSONRequestBody defines body for ChunkText for application/json ContentType. type ChunkTextJSONRequestBody = ChunkRequest -// DecodeTextJSONRequestBody defines body for DecodeText for application/json ContentType. -type DecodeTextJSONRequestBody = DecodeRequest - // GenerateEmbeddingsJSONRequestBody defines body for GenerateEmbeddings for application/json ContentType. type GenerateEmbeddingsJSONRequestBody = EmbedRequest @@ -1116,9 +1071,6 @@ type ServerInterface interface { // Chunk text into smaller segments // (POST /chunk) ChunkText(w http.ResponseWriter, r *http.Request) - // Decode text from embeddings using T5Gemma-2 - // (POST /decode) - DecodeText(w http.ResponseWriter, r *http.Request) // Generate embeddings // (POST /embed) GenerateEmbeddings(w http.ResponseWriter, r *http.Request) @@ -1165,20 +1117,6 @@ func (siw *ServerInterfaceWrapper) ChunkText(w http.ResponseWriter, r *http.Requ handler.ServeHTTP(w, r) } -// DecodeText operation middleware -func (siw *ServerInterfaceWrapper) DecodeText(w http.ResponseWriter, r *http.Request) { - - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.DecodeText(w, r) - })) - - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) -} - // GenerateEmbeddings operation middleware func (siw *ServerInterfaceWrapper) GenerateEmbeddings(w http.ResponseWriter, r *http.Request) { @@ -1398,7 +1336,6 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H } m.HandleFunc("POST "+options.BaseURL+"/chunk", wrapper.ChunkText) - m.HandleFunc("POST "+options.BaseURL+"/decode", wrapper.DecodeText) m.HandleFunc("POST "+options.BaseURL+"/embed", wrapper.GenerateEmbeddings) m.HandleFunc("POST "+options.BaseURL+"/generate", wrapper.GenerateContent) m.HandleFunc("GET "+options.BaseURL+"/models", wrapper.ListModels) @@ -1413,210 +1350,200 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "cx7df7r/vwEAAP//LcZSyv23AAA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/pkg/termite/api.go b/pkg/termite/api.go index 197e2ff..b271971 100644 --- a/pkg/termite/api.go +++ b/pkg/termite/api.go @@ -35,7 +35,6 @@ import ( "github.com/antflydb/termite/pkg/termite/lib/embeddings" "github.com/antflydb/termite/pkg/termite/lib/generation" "github.com/antflydb/termite/pkg/termite/lib/ner" - "github.com/antflydb/termite/pkg/termite/lib/seq2seq" "github.com/bytedance/sonic/decoder" "github.com/bytedance/sonic/encoder" "go.uber.org/zap" @@ -91,11 +90,6 @@ func (t *TermiteAPI) RewriteText(w http.ResponseWriter, r *http.Request) { t.node.handleApiRewrite(w, r) } -// DecodeText implements ServerInterface - embedding-to-text generation -func (t *TermiteAPI) DecodeText(w http.ResponseWriter, r *http.Request) { - t.node.handleApiDecode(w, r) -} - // ListModels implements ServerInterface func (t *TermiteAPI) ListModels(w http.ResponseWriter, r *http.Request) { resp := ModelsResponse{ @@ -1396,153 +1390,3 @@ func (ln *TermiteNode) handleApiRewrite(w http.ResponseWriter, r *http.Request) } } -// handleApiDecode handles POST /api/decode for embedding-to-text generation. -// It supports two modes: -// - Text prompt: standard text generation using the seq2seq model -// - Embeddings: direct decoding from pre-computed encoder hidden states -func (ln *TermiteNode) handleApiDecode(w http.ResponseWriter, r *http.Request) { - defer func() { _ = r.Body.Close() }() - - // Check if seq2seq registry is available - if ln.seq2seqRegistry == nil { - http.Error(w, "decode not available: seq2seq models not loaded", http.StatusServiceUnavailable) - return - } - - // Apply backpressure - release, err := ln.requestQueue.Acquire(r.Context()) - if err != nil { - ln.logger.Error("failed to acquire request slot", zap.Error(err)) - http.Error(w, "service overloaded", http.StatusServiceUnavailable) - return - } - defer release() - - // Decode request - var req DecodeRequest - if err := decoder.NewStreamDecoder(r.Body).Decode(&req); err != nil { - ln.logger.Error("decoding decode request", zap.Error(err)) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - // Validate request - if req.Model == "" { - http.Error(w, "model is required", http.StatusBadRequest) - return - } - - // Validate: prompt XOR embeddings - hasPrompt := req.Prompt != "" - hasEmbeddings := len(req.Embeddings) > 0 - if !hasPrompt && !hasEmbeddings { - http.Error(w, "either prompt or embeddings is required", http.StatusBadRequest) - return - } - if hasPrompt && hasEmbeddings { - http.Error(w, "prompt and embeddings are mutually exclusive", http.StatusBadRequest) - return - } - - // Get model from registry - model, err := ln.seq2seqRegistry.Get(req.Model) - if err != nil { - ln.logger.Error("failed to get decode model", - zap.String("model", req.Model), - zap.Error(err)) - http.Error(w, fmt.Sprintf("model not found: %s", req.Model), http.StatusNotFound) - return - } - - var output *seq2seq.GeneratedOutput - - if hasEmbeddings { - // Embedding-to-text path: decode from pre-computed embeddings - dec, ok := model.(seq2seq.Decoder) - if !ok { - http.Error(w, "model does not support embedding decoding", http.StatusBadRequest) - return - } - - hiddenSize := dec.HiddenSize() - - // Validate embedding dimensions - for i, emb := range req.Embeddings { - if len(emb) != hiddenSize { - http.Error(w, fmt.Sprintf("embedding %d has wrong dimension: got %d, expected %d", - i, len(emb), hiddenSize), http.StatusBadRequest) - return - } - } - - // Build decode options - opts := seq2seq.DefaultDecodeOptions() - if req.MaxTokens > 0 { - opts.MaxTokens = req.MaxTokens - } - if req.Temperature > 0 { - opts.Temperature = req.Temperature - } - if req.TopP > 0 { - opts.TopP = req.TopP - } - - // Build decoder input - input := &seq2seq.DecoderInput{ - EncoderHiddenStates: req.Embeddings, - HiddenSize: hiddenSize, - } - if len(req.AttentionMask) > 0 { - // Convert []int to []int64 - input.AttentionMask = make([]int64, len(req.AttentionMask)) - for i, v := range req.AttentionMask { - input.AttentionMask[i] = int64(v) - } - } - - ln.logger.Debug("decoding from embeddings", - zap.String("model", req.Model), - zap.Int("seq_len", len(req.Embeddings)), - zap.Int("hidden_size", hiddenSize)) - - output, err = dec.DecodeFromEmbeddings(r.Context(), input, opts) - } else { - // Text prompt path: standard generation - ln.logger.Debug("decoding from prompt", - zap.String("model", req.Model), - zap.String("prompt", req.Prompt)) - - output, err = model.Generate(r.Context(), []string{req.Prompt}) - } - - if err != nil { - ln.logger.Error("decode failed", - zap.String("model", req.Model), - zap.Error(err)) - http.Error(w, fmt.Sprintf("decode failed: %v", err), http.StatusInternalServerError) - return - } - - ln.logger.Info("decode request completed", - zap.String("model", req.Model), - zap.Int("num_outputs", len(output.Texts))) - - // Build response - flatten nested texts - var texts []string - for _, batch := range output.Texts { - texts = append(texts, batch...) - } - - resp := DecodeResponse{ - Model: req.Model, - Texts: texts, - } - - w.Header().Set("Content-Type", "application/json") - if err := encoder.NewStreamEncoder(w).Encode(resp); err != nil { - ln.logger.Error("encoding response", zap.Error(err)) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } -} - diff --git a/pkg/termite/lib/seq2seq/decoder.go b/pkg/termite/lib/seq2seq/decoder.go deleted file mode 100644 index fbb4a21..0000000 --- a/pkg/termite/lib/seq2seq/decoder.go +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright 2025 Antfly, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package seq2seq - -import ( - "context" -) - -// DecoderInput represents input for embedding-to-text generation. -// This allows bypassing the encoder and generating text directly from -// pre-computed embeddings (encoder hidden states). -type DecoderInput struct { - // EncoderHiddenStates contains the pre-computed encoder outputs. - // Shape: [sequence_length, hidden_size] for a single input. - // These are passed directly to the decoder as encoder_hidden_states. - EncoderHiddenStates [][]float32 - - // AttentionMask is an optional mask for the encoder hidden states. - // Shape: [sequence_length]. If nil, all positions are attended to. - AttentionMask []int64 - - // HiddenSize is the dimension of the hidden states (for validation). - // This should match the model's hidden_size configuration. - HiddenSize int -} - -// DecodeOptions configures the decoding/generation behavior. -type DecodeOptions struct { - // MaxTokens is the maximum number of tokens to generate. - // Default: 256 - MaxTokens int - - // Temperature controls randomness in sampling. Higher values (e.g., 1.0) - // produce more diverse output, lower values (e.g., 0.1) produce more - // deterministic output. Default: 1.0 - Temperature float32 - - // TopP is the nucleus sampling probability threshold. Tokens are sampled - // from the smallest set of tokens whose cumulative probability exceeds TopP. - // Default: 1.0 (disabled) - TopP float32 - - // TopK limits sampling to the top K most likely tokens. - // Default: 0 (disabled) - TopK int - - // RepetitionPenalty penalizes repeated tokens. Values > 1.0 discourage - // repetition, values < 1.0 encourage it. Default: 1.0 - RepetitionPenalty float32 -} - -// DefaultDecodeOptions returns sensible default options for decoding. -func DefaultDecodeOptions() DecodeOptions { - return DecodeOptions{ - MaxTokens: 256, - Temperature: 1.0, - TopP: 1.0, - TopK: 0, - RepetitionPenalty: 1.2, - } -} - -// Decoder is the interface for models that support generating text from embeddings. -// This enables embedding-to-text generation workflows, including: -// - Embedding inversion (reconstructing text from embeddings) -// - Custom embedding injection (using external/manipulated embeddings) -// - Cross-modal generation (vision embeddings to text) -type Decoder interface { - // DecodeFromEmbeddings generates text from pre-computed encoder hidden states. - // This bypasses the encoder and runs only the decoder, using the provided - // embeddings as encoder_hidden_states for cross-attention. - DecodeFromEmbeddings(ctx context.Context, input *DecoderInput, opts DecodeOptions) (*GeneratedOutput, error) - - // HiddenSize returns the expected embedding dimension for validation. - // Embeddings passed to DecodeFromEmbeddings must have this dimension. - HiddenSize() int - - // Close releases resources held by the decoder. - Close() error -} diff --git a/pkg/termite/lib/seq2seq/t5gemma2.go b/pkg/termite/lib/seq2seq/t5gemma2.go index 1632b8d..e1bbc59 100644 --- a/pkg/termite/lib/seq2seq/t5gemma2.go +++ b/pkg/termite/lib/seq2seq/t5gemma2.go @@ -30,7 +30,6 @@ import ( "os" "path/filepath" "strings" - "sync" "github.com/antflydb/termite/pkg/termite/lib/hugot" khugot "github.com/knights-analytics/hugot" @@ -41,9 +40,8 @@ import ( _ "golang.org/x/image/webp" ) -// Ensure T5Gemma2Generator implements the Model and Decoder interfaces +// Ensure T5Gemma2Generator implements the Model interface var _ Model = (*T5Gemma2Generator)(nil) -var _ Decoder = (*T5Gemma2Generator)(nil) // T5Gemma2Generator implements multimodal seq2seq text generation using T5Gemma-2. // It supports text-to-text and image+text-to-text generation. @@ -62,10 +60,7 @@ type T5Gemma2Generator struct { modelPath string pipelineName string // pipeline name for hugot registry cleanup - // Lazy loading for vision encoder - hasVisionFile bool // true if vision_encoder.onnx exists - visionOnce sync.Once // ensures vision pipeline loaded only once - visionLoadErr error // captures any error during lazy load + hasVisionFile bool // true if vision_encoder.onnx exists } // T5Gemma2GeneratorConfig holds configuration for T5Gemma-2 generation @@ -193,13 +188,51 @@ func NewT5Gemma2GeneratorWithSession(modelPath string, sharedSession *khugot.Ses return nil, fmt.Errorf("creating Seq2Seq pipeline: %w", err) } - // Vision pipeline is loaded lazily on first image request to save memory. - // Only log whether the file exists, don't load it yet. + // Load vision pipeline eagerly if available. + // The vision encoder is required for correct text generation behavior, + // even when only processing text (discovered in previous testing). + var visionPipeline *pipelines.FeatureExtractionPipeline if hasVision { - logger.Info("T5Gemma-2 generator initialized (vision available, will load on first use)", + logger.Info("Loading vision encoder pipeline", + zap.String("modelPath", modelPath)) + + imageSize := config.ImageSize + if imageSize == 0 { + imageSize = 896 // Default for T5Gemma-2 (SigLIP) + } + + visionPipelineName := fmt.Sprintf("t5gemma2-vision:%s", filepath.Base(modelPath)) + visionConfig := khugot.FeatureExtractionConfig{ + ModelPath: modelPath, + Name: visionPipelineName, + OnnxFilename: "vision_encoder.onnx", + Options: []backends.PipelineOption[*pipelines.FeatureExtractionPipeline]{ + pipelines.WithImageMode(), + pipelines.WithPreprocessSteps[*pipelines.FeatureExtractionPipeline]( + imageutil.ResizeStep(imageSize), + imageutil.CenterCropStep(imageSize, imageSize), + ), + pipelines.WithNormalizationSteps[*pipelines.FeatureExtractionPipeline]( + imageutil.RescaleStep(), + imageutil.CLIPPixelNormalizationStep(), + ), + pipelines.WithNCHWFormat[*pipelines.FeatureExtractionPipeline](), + }, + } + + var err error + visionPipeline, err = khugot.NewPipeline(session, visionConfig) + if err != nil { + if !sessionShared { + session.Destroy() + } + return nil, fmt.Errorf("creating vision pipeline: %w", err) + } + + logger.Info("T5Gemma-2 generator initialized (with vision encoder)", zap.String("task", config.Task), zap.Int("max_new_tokens", config.MaxNewTokens), - zap.Int("imageSize", config.ImageSize)) + zap.Int("imageSize", imageSize)) } else { logger.Info("T5Gemma-2 generator initialized (text-only mode)", zap.String("task", config.Task), @@ -209,7 +242,7 @@ func NewT5Gemma2GeneratorWithSession(modelPath string, sharedSession *khugot.Ses return &T5Gemma2Generator{ session: session, seq2seqPipeline: seq2seqPipeline, - visionPipeline: nil, // Loaded lazily + visionPipeline: visionPipeline, logger: logger, sessionShared: sessionShared, config: config, @@ -382,53 +415,13 @@ func (g *T5Gemma2Generator) IsVisionLoaded() bool { return g.visionPipeline != nil } -// loadVisionPipeline lazily loads the vision encoder pipeline. -// Thread-safe: uses sync.Once to ensure only one load attempt. +// loadVisionPipeline checks that the vision encoder pipeline is available. +// The vision pipeline is loaded eagerly at initialization time. func (g *T5Gemma2Generator) loadVisionPipeline() error { - g.visionOnce.Do(func() { - if !g.hasVisionFile { - g.visionLoadErr = errors.New("vision encoder not available: vision_encoder.onnx not found") - return - } - - g.logger.Info("Lazily loading vision encoder pipeline", - zap.String("modelPath", g.modelPath)) - - imageSize := g.config.ImageSize - if imageSize == 0 { - imageSize = 896 // Default for T5Gemma-2 (SigLIP) - } - - visionPipelineName := fmt.Sprintf("t5gemma2-vision:%s", filepath.Base(g.modelPath)) - visionConfig := khugot.FeatureExtractionConfig{ - ModelPath: g.modelPath, - Name: visionPipelineName, - OnnxFilename: "vision_encoder.onnx", - Options: []backends.PipelineOption[*pipelines.FeatureExtractionPipeline]{ - pipelines.WithImageMode(), - pipelines.WithPreprocessSteps[*pipelines.FeatureExtractionPipeline]( - imageutil.ResizeStep(imageSize), - imageutil.CenterCropStep(imageSize, imageSize), - ), - pipelines.WithNormalizationSteps[*pipelines.FeatureExtractionPipeline]( - imageutil.RescaleStep(), - imageutil.CLIPPixelNormalizationStep(), - ), - pipelines.WithNCHWFormat[*pipelines.FeatureExtractionPipeline](), - }, - } - - pipeline, err := khugot.NewPipeline(g.session, visionConfig) - if err != nil { - g.visionLoadErr = fmt.Errorf("loading vision pipeline: %w", err) - return - } - - g.visionPipeline = pipeline - g.logger.Info("Vision encoder pipeline loaded successfully", - zap.Int("imageSize", imageSize)) - }) - return g.visionLoadErr + if g.visionPipeline == nil { + return errors.New("vision encoder not available: vision_encoder.onnx not found or failed to load") + } + return nil } // Config returns the generator configuration. @@ -436,80 +429,6 @@ func (g *T5Gemma2Generator) Config() T5Gemma2GeneratorConfig { return g.config } -// HiddenSize returns the expected embedding dimension for the model. -// This is used to validate embeddings passed to DecodeFromEmbeddings. -func (g *T5Gemma2Generator) HiddenSize() int { - return g.config.HiddenSize -} - -// DecodeFromEmbeddings generates text from pre-computed encoder hidden states. -// This enables embedding-to-text generation workflows where custom embeddings -// (from external sources, manipulated vectors, or vision encoders) are used -// directly for text generation without running the encoder. -// -// The embeddings are used as encoder_hidden_states in the decoder's cross-attention -// mechanism, allowing the decoder to generate text conditioned on these embeddings. -// -// Example use cases: -// - Embedding inversion: Reconstruct text from embeddings -// - Custom embedding injection: Use manipulated or external embeddings -// - Cross-modal generation: Convert vision embeddings to text descriptions -func (g *T5Gemma2Generator) DecodeFromEmbeddings( - ctx context.Context, - input *DecoderInput, - opts DecodeOptions, -) (*GeneratedOutput, error) { - if len(input.EncoderHiddenStates) == 0 { - return nil, errors.New("encoder hidden states are required") - } - - select { - case <-ctx.Done(): - return nil, ctx.Err() - default: - } - - // Validate embedding dimensions - expectedDim := g.config.HiddenSize - for i, emb := range input.EncoderHiddenStates { - if len(emb) != expectedDim { - return nil, fmt.Errorf("embedding %d dimension mismatch: got %d, expected %d", - i, len(emb), expectedDim) - } - } - - g.logger.Debug("Starting T5Gemma-2 decode from embeddings", - zap.Int("num_embeddings", len(input.EncoderHiddenStates)), - zap.Int("hidden_size", expectedDim), - zap.Int("max_tokens", opts.MaxTokens)) - - // Apply default options if not specified - if opts.MaxTokens <= 0 { - opts.MaxTokens = g.config.MaxNewTokens - } - if opts.Temperature <= 0 { - opts.Temperature = g.config.Temperature - } - if opts.TopP <= 0 { - opts.TopP = g.config.TopP - } - if opts.RepetitionPenalty <= 0 { - opts.RepetitionPenalty = g.config.RepetitionPenalty - } - - // Run generation using the decoder with custom encoder hidden states - output, err := g.runDecoderWithEmbeddings(ctx, input, opts) - if err != nil { - g.logger.Error("T5Gemma-2 decode from embeddings failed", zap.Error(err)) - return nil, fmt.Errorf("decoding from embeddings: %w", err) - } - - g.logger.Debug("T5Gemma-2 decode from embeddings completed", - zap.Int("num_outputs", len(output.Texts))) - - return output, nil -} - // Close releases resources. func (g *T5Gemma2Generator) Close() error { var errs []error diff --git a/pkg/termite/lib/seq2seq/t5gemma2_decoder.go b/pkg/termite/lib/seq2seq/t5gemma2_decoder.go deleted file mode 100644 index cdb392c..0000000 --- a/pkg/termite/lib/seq2seq/t5gemma2_decoder.go +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright 2025 Antfly, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//go:build onnx && ORT - -package seq2seq - -import ( - "context" - "fmt" - "os" - "path/filepath" - - "github.com/knights-analytics/hugot/backends" - "github.com/knights-analytics/hugot/pipelines" - ort "github.com/yalue/onnxruntime_go" - "go.uber.org/zap" -) - -// runDecoderWithEmbeddings runs the decoder directly with pre-computed encoder hidden states. -// This bypasses the encoder and uses the embeddings as encoder_hidden_states directly -// for the decoder's cross-attention mechanism. -// -// The implementation creates ONNX tensors from the input embeddings and uses hugot's -// generation functions to run the autoregressive decoding loop. -func (g *T5Gemma2Generator) runDecoderWithEmbeddings( - ctx context.Context, - input *DecoderInput, - opts DecodeOptions, -) (*GeneratedOutput, error) { - // Verify decoder ONNX files exist - decoderInitPath := filepath.Join(g.modelPath, "decoder-init.onnx") - decoderPath := filepath.Join(g.modelPath, "decoder.onnx") - - if _, err := os.Stat(decoderInitPath); err != nil { - return nil, fmt.Errorf("decoder-init.onnx not found: %w", err) - } - if _, err := os.Stat(decoderPath); err != nil { - return nil, fmt.Errorf("decoder.onnx not found: %w", err) - } - - // Check context cancellation - select { - case <-ctx.Done(): - return nil, ctx.Err() - default: - } - - // Prepare embeddings dimensions - batchSize := 1 // Currently support single-input decoding - seqLen := len(input.EncoderHiddenStates) - hiddenSize := input.HiddenSize - - g.logger.Debug("Running decoder with embeddings", - zap.Int("batch_size", batchSize), - zap.Int("seq_len", seqLen), - zap.Int("hidden_size", hiddenSize), - zap.Int("max_tokens", opts.MaxTokens)) - - // Flatten embeddings to [batch_size * seq_len * hidden_size] - flatEmbeddings := make([]float32, batchSize*seqLen*hiddenSize) - for i, emb := range input.EncoderHiddenStates { - copy(flatEmbeddings[i*hiddenSize:], emb) - } - - // Create encoder_hidden_states tensor [batch_size, seq_len, hidden_size] - encoderHiddenStatesTensor, err := ort.NewTensor( - ort.NewShape(int64(batchSize), int64(seqLen), int64(hiddenSize)), - flatEmbeddings, - ) - if err != nil { - return nil, fmt.Errorf("creating encoder_hidden_states tensor: %w", err) - } - - // Track whether tensors have been transferred to batch ownership. - // If any operation fails before batch takes ownership, defers will clean up. - tensorsOwnedByBatch := false - defer func() { - if !tensorsOwnedByBatch { - encoderHiddenStatesTensor.Destroy() - } - }() - - // Create attention mask - var attentionMask []int64 - if input.AttentionMask != nil && len(input.AttentionMask) > 0 { - attentionMask = input.AttentionMask - } else { - // Default: attend to all positions - attentionMask = make([]int64, seqLen) - for i := range attentionMask { - attentionMask[i] = 1 - } - } - - // Create attention mask tensor [batch_size, seq_len] - flatAttentionMask := make([]int64, batchSize*seqLen) - copy(flatAttentionMask, attentionMask) - - encoderAttentionMaskTensor, err := ort.NewTensor( - ort.NewShape(int64(batchSize), int64(seqLen)), - flatAttentionMask, - ) - if err != nil { - return nil, fmt.Errorf("creating encoder_attention_mask tensor: %w", err) - } - defer func() { - if !tensorsOwnedByBatch { - encoderAttentionMaskTensor.Destroy() - } - }() - - // Create a Seq2SeqBatch and set encoder outputs directly - batch := pipelines.NewSeq2SeqBatch(batchSize) - batch.MaxInputLength = seqLen - batch.SetEncoderHiddenStates(encoderHiddenStatesTensor) - batch.SetEncoderAttentionMask(encoderAttentionMaskTensor) - - // Set cleanup function for encoder tensors - batch now owns the tensors - batch.SetDestroyEncoder(func() error { - err1 := encoderHiddenStatesTensor.Destroy() - err2 := encoderAttentionMaskTensor.Destroy() - if err1 != nil { - return err1 - } - return err2 - }) - tensorsOwnedByBatch = true // Tensors are now managed by batch.Destroy() - - // Ensure cleanup on exit - defer func() { - if destroyErr := batch.Destroy(); destroyErr != nil { - g.logger.Warn("Error destroying batch", zap.Error(destroyErr)) - } - }() - - // Run generation using hugot's backend functions - // The seq2seqPipeline implements Seq2SeqPipelineInterface - var genErr error - if opts.Temperature > 0 && opts.Temperature != 1.0 { - // Use sampling with temperature - genErr = backends.RunSeq2SeqGenerationSampling(batch, g.seq2seqPipeline) - } else { - // Use greedy decoding - genErr = backends.RunSeq2SeqGenerationGreedy(batch, g.seq2seqPipeline) - } - - if genErr != nil { - return nil, fmt.Errorf("generation failed: %w", genErr) - } - - // Get generated tokens and decode to text - generatedTokens := batch.GetGeneratedTokens() - - g.logger.Debug("Generation completed", - zap.Int("num_sequences", len(generatedTokens))) - - // Decode tokens to text using the pipeline's tokenizer - texts := make([][]string, len(generatedTokens)) - for i, tokens := range generatedTokens { - // Convert int64 tokens to uint32 for decode function - uint32Tokens := make([]uint32, len(tokens)) - for j, t := range tokens { - uint32Tokens[j] = uint32(t) - } - - // Decode tokens, skipping special tokens - tokenizer := g.seq2seqPipeline.GetTokenizer() - text, decodeErr := backends.Decode(uint32Tokens, tokenizer, true) - if decodeErr != nil { - return nil, fmt.Errorf("decoding tokens: %w", decodeErr) - } - texts[i] = []string{text} - } - - // Convert tokens to uint32 format for output - tokenOutput := make([][][]uint32, len(generatedTokens)) - for i, tokens := range generatedTokens { - uint32Tokens := make([]uint32, len(tokens)) - for j, t := range tokens { - uint32Tokens[j] = uint32(t) - } - tokenOutput[i] = [][]uint32{uint32Tokens} - } - - return &GeneratedOutput{ - Texts: texts, - Tokens: tokenOutput, - }, nil -} diff --git a/pkg/termite/lib/seq2seq/t5gemma2_stub.go b/pkg/termite/lib/seq2seq/t5gemma2_stub.go index 871a18d..38c8a18 100644 --- a/pkg/termite/lib/seq2seq/t5gemma2_stub.go +++ b/pkg/termite/lib/seq2seq/t5gemma2_stub.go @@ -98,21 +98,6 @@ func (g *T5Gemma2Generator) Close() error { return nil } -// HiddenSize returns the expected embedding dimension. -// Stub returns 0 as ONNX is not available. -func (g *T5Gemma2Generator) HiddenSize() int { - return 0 -} - -// DecodeFromEmbeddings is not available without ONNX Runtime. -func (g *T5Gemma2Generator) DecodeFromEmbeddings( - ctx context.Context, - input *DecoderInput, - opts DecodeOptions, -) (*GeneratedOutput, error) { - return nil, errors.New("T5Gemma-2 decode from embeddings not available: build with -tags=\"onnx,ORT\"") -} - // IsT5Gemma2GeneratorModel checks if a model directory contains T5Gemma-2 generator files. func IsT5Gemma2GeneratorModel(modelPath string) bool { // Must have standard seq2seq files diff --git a/pkg/termite/openapi.yaml b/pkg/termite/openapi.yaml index bfd8a72..66227bb 100644 --- a/pkg/termite/openapi.yaml +++ b/pkg/termite/openapi.yaml @@ -497,77 +497,6 @@ components: type: string description: Rewritten texts (array of arrays, one per input, multiple per beam) - # Decode Types (embedding-to-text generation) - DecodeRequest: - type: object - required: - - model - properties: - model: - type: string - description: Name of T5Gemma-2 or compatible decoder model - example: google/t5gemma-2-270m - prompt: - type: string - description: Input prompt for text generation (mutually exclusive with embeddings) - example: "The meaning of life is" - embeddings: - type: array - items: - type: array - items: - type: number - format: float - description: | - Pre-computed encoder hidden states for embedding-to-text generation. - Shape: [sequence_length, hidden_size] for single input. - When provided, the decoder generates text directly from these embeddings - without running the encoder. Mutually exclusive with 'prompt'. - sequence_length: - type: integer - description: The sequence length of the embeddings (for reshaping flat arrays) - attention_mask: - type: array - items: - type: integer - description: Optional attention mask for embeddings. Shape [sequence_length]. - max_tokens: - type: integer - description: Maximum number of tokens to generate (default 256) - default: 256 - temperature: - type: number - format: float - description: Sampling temperature (default 1.0) - default: 1.0 - top_p: - type: number - format: float - description: Nucleus sampling probability (default 1.0) - default: 1.0 - - DecodeResponse: - type: object - required: - - model - - texts - properties: - model: - type: string - description: Name of model used for decoding - texts: - type: array - items: - type: string - description: Generated text sequences - tokens: - type: array - items: - type: array - items: - type: integer - description: Token IDs for each generated sequence (optional) - # Models Types ModelsResponse: type: object @@ -1672,81 +1601,6 @@ paths: schema: $ref: "#/components/schemas/Error" - /decode: - post: - summary: Decode text from embeddings using T5Gemma-2 - description: | - Generate text using the T5Gemma-2 decoder from pre-computed embeddings or text prompts. - This enables embedding-to-text generation for custom embedding injection workflows. - - ## Models - - - Uses T5Gemma-2 models from `models_dir/rewriters/` that have decoder components - - Requires decoder.onnx and decoder-init.onnx files - - ## Use Cases - - - **Text Continuation**: Generate text continuations from prompts - - **Embedding Inversion**: Reconstruct text from embeddings - - **Guided Generation**: Generate text conditioned on custom encoder outputs - - **Cross-modal Generation**: Generate text from vision embeddings - - ## Example (with prompt) - - ```json - { - "model": "google/t5gemma-2-270m", - "prompt": "The meaning of life is" - } - ``` - - ## Example (with embeddings) - - ```json - { - "model": "google/t5gemma-2-270m", - "embeddings": [[0.1, 0.2, ...], [0.3, 0.4, ...]] - } - ``` - operationId: decodeText - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/DecodeRequest" - responses: - "200": - description: Text decoded successfully - content: - application/json: - schema: - $ref: "#/components/schemas/DecodeResponse" - "400": - description: Invalid request - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - "404": - description: Model not found or does not support decoding - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - "500": - description: Internal server error - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - "503": - description: Decode service unavailable (no compatible models) - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - /models: get: summary: List available models diff --git a/scripts/export_t5gemma2.py b/scripts/export_t5gemma2.py index dd84819..1fea622 100755 --- a/scripts/export_t5gemma2.py +++ b/scripts/export_t5gemma2.py @@ -33,14 +33,14 @@ 4. decoder.onnx - Decoder with past_key_values (efficient generation) Usage: - # Export 270M variant (recommended for development) - ./scripts/export_t5gemma2.py --model google/t5gemma-2-270m-270m --output ./models/t5gemma2/270m + # Export 270M variant (recommended for development) - uses default output path + ./scripts/export_t5gemma2.py --model google/t5gemma-2-270m-270m # Export with testing - ./scripts/export_t5gemma2.py --model google/t5gemma-2-270m-270m --output ./models/t5gemma2/270m --test + ./scripts/export_t5gemma2.py --model google/t5gemma-2-270m-270m --test # Export 1B variant - ./scripts/export_t5gemma2.py --model google/t5gemma-2-1b-1b --output ./models/t5gemma2/1b + ./scripts/export_t5gemma2.py --model google/t5gemma-2-1b-1b --output ~/.termite/models/rewriters/google/t5gemma-2-1b Available Models: - google/t5gemma-2-270m-270m (~800M total params, fast) @@ -807,7 +807,7 @@ def get_config_value(config, *paths, default=None): "model_id": model_id, "model_type": "t5gemma2", "task": "multimodal_seq2seq", - "capabilities": ["embeddings", "generation", "decoding", "multimodal"], + "capabilities": ["embeddings", "generation", "multimodal"], "max_encoder_length": 131072, # 128K "max_decoder_length": 32768, # 32K output "hidden_size": hidden_size, @@ -960,7 +960,6 @@ def get_config_value(config, *paths, default=None): if has_vision: logger.info(" - Image embeddings: vision_encoder.onnx") logger.info(" - Text generation: encoder.onnx + decoder-init.onnx") - logger.info(" - Decode from embeddings: decoder-init.onnx (with encoder_hidden_states)") def test_exported_model(output_dir: str, test_text: Optional[str] = None) -> None: @@ -1076,14 +1075,14 @@ def main(): formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: - # Export 270M variant (recommended for development) - ./scripts/export_t5gemma2.py --model google/t5gemma-2-270m-270m --output ./models/t5gemma2/270m + # Export 270M variant (recommended for development) - uses default path + ./scripts/export_t5gemma2.py --model google/t5gemma-2-270m-270m # Export with testing - ./scripts/export_t5gemma2.py --model google/t5gemma-2-270m-270m --output ./models/t5gemma2/270m --test + ./scripts/export_t5gemma2.py --model google/t5gemma-2-270m-270m --test - # Export 1B variant - ./scripts/export_t5gemma2.py --model google/t5gemma-2-1b-1b --output ./models/t5gemma2/1b + # Export 1B variant with custom path + ./scripts/export_t5gemma2.py --model google/t5gemma-2-1b-1b --output ~/.termite/models/rewriters/google/t5gemma-2-1b Available Models: - google/t5gemma-2-270m-270m (~800M total params, fast) @@ -1109,8 +1108,8 @@ def main(): parser.add_argument( "-o", "--output", type=str, - default="./models/t5gemma2/270m", - help="Output directory for the exported model", + default=os.path.expanduser("~/.termite/models/rewriters/google/t5gemma-2-270m"), + help="Output directory for the exported model (default: ~/.termite/models/rewriters/google/t5gemma-2-270m)", ) parser.add_argument( "--test", From ef3056df11ecade097be4f8d6774ee659b48edc0 Mon Sep 17 00:00:00 2001 From: timkaye11 Date: Thu, 8 Jan 2026 16:08:04 -0800 Subject: [PATCH 7/7] update go mods --- go.work | 3 +-- go.work.sum | 6 ++---- pkg/termite/go.mod | 2 +- pkg/termite/go.sum | 4 ++-- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/go.work b/go.work index 4b1d6cc..f18056d 100644 --- a/go.work +++ b/go.work @@ -1,7 +1,6 @@ go 1.25.5 use ( - ../hugot ./e2e ./pkg/client ./pkg/operator @@ -13,6 +12,6 @@ replace github.com/gomlx/gomlx => github.com/timkaye11/gomlx v0.0.0-202512210449 replace github.com/gomlx/onnx-gomlx => github.com/timkaye11/onnx-gomlx v0.0.0-20251217194823-a871bb8cc687 -replace github.com/knights-analytics/hugot => github.com/ajroetker/hugot v0.0.0-20260107061453-f83ae7b7c356 +replace github.com/knights-analytics/hugot => github.com/timkaye11/hugot v0.0.0-20260108205439-6127b8a4c499 replace github.com/knights-analytics/ortgenai => github.com/ajroetker/ortgenai v0.0.0-20260102002309-90abcfc27670 diff --git a/go.work.sum b/go.work.sum index 0bc772b..e9931bd 100644 --- a/go.work.sum +++ b/go.work.sum @@ -49,8 +49,6 @@ github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMo github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06/go.mod h1:7erjKLwalezA0k99cWs5L11HWOAPNjdUZ6RxH1BXbbM= github.com/ajroetker/hugot v0.0.0-20251229040509-3dc1031934d6 h1:sMCUPp1t4ClAZ7RoBfbF9mFPZ4fwu3/TktqtXj7dyrs= github.com/ajroetker/hugot v0.0.0-20251229040509-3dc1031934d6/go.mod h1:HM0TApRamY//nlzNqZCIlP1muD8mvkdYSka3bmRrm7Y= -github.com/ajroetker/hugot v0.0.0-20260107061453-f83ae7b7c356 h1:opUqf468UDMKDFEmmQZybIi5s7egT0RZGZX+WeylE8A= -github.com/ajroetker/hugot v0.0.0-20260107061453-f83ae7b7c356/go.mod h1:I3/sobJjSQecs1xBUKnbjBKcbaX03uQ7gfkDW55zAU8= github.com/ajroetker/ortgenai v0.0.0-20251229052243-d2d8091589c4/go.mod h1:zolhST8PioaCCsnwHBUesIBKU4CW/ZrF169YjFfg7BY= github.com/ajroetker/ortgenai v0.0.0-20260101101217-6423af4e4878 h1:5tWcPKi9eDbiaW3hDw+vwIupoz1nJT7xEFtLKB60Vxk= github.com/ajroetker/ortgenai v0.0.0-20260101101217-6423af4e4878/go.mod h1:zolhST8PioaCCsnwHBUesIBKU4CW/ZrF169YjFfg7BY= @@ -87,7 +85,6 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/erkkah/margaid v0.3.0/go.mod h1:rf8vNecfnMZbhvzml9y+UspzrsmvUyS0bfkLVNPNCQY= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= @@ -221,6 +218,8 @@ github.com/timkaye11/gomlx v0.0.0-20251218235241-74992cedba51 h1:esopQIaYzkoJWVm github.com/timkaye11/gomlx v0.0.0-20251218235241-74992cedba51/go.mod h1:HKSGc4Btb3VntH/BV/UsJYzgTMbLT/Gk69kv95rjruM= github.com/timkaye11/hugot v0.0.0-20251229042211-73e5b84f666d h1:NpwiXABAYQogTUTHHa70UzICllLEjZNfJHQmEm00854= github.com/timkaye11/hugot v0.0.0-20251229042211-73e5b84f666d/go.mod h1:HM0TApRamY//nlzNqZCIlP1muD8mvkdYSka3bmRrm7Y= +github.com/timkaye11/hugot v0.0.0-20260108205439-6127b8a4c499 h1:f5+W2JKwNwLLkYzZ+A+Wcv9shdFSffo1L8zasfaIUAw= +github.com/timkaye11/hugot v0.0.0-20260108205439-6127b8a4c499/go.mod h1:F8q/Zn0G8jUFUCp6KAuUvQRRsHZk3VEb0Efo0c6z/N8= github.com/timkaye11/onnx-gomlx v0.0.0-20251216183743-5f89a03da602 h1:+5JuKeEiKoZ8BONRE4HDEkdOGl62X7CTv18wk3N/uiA= github.com/timkaye11/onnx-gomlx v0.0.0-20251216183743-5f89a03da602/go.mod h1:RjCGoxroeaa4IQc7TN+XeUJRwaVRfZ7TjRm+gtS/Z/U= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= @@ -372,7 +371,6 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/pkg/termite/go.mod b/pkg/termite/go.mod index 62b31a8..3080c0f 100644 --- a/pkg/termite/go.mod +++ b/pkg/termite/go.mod @@ -6,7 +6,7 @@ replace github.com/gomlx/gomlx => github.com/timkaye11/gomlx v0.0.0-202512210449 replace github.com/gomlx/onnx-gomlx => github.com/timkaye11/onnx-gomlx v0.0.0-20251217194823-a871bb8cc687 -replace github.com/knights-analytics/hugot => github.com/ajroetker/hugot v0.0.0-20260107061453-f83ae7b7c356 +replace github.com/knights-analytics/hugot => github.com/timkaye11/hugot v0.0.0-20260108205439-6127b8a4c499 replace github.com/knights-analytics/ortgenai => github.com/ajroetker/ortgenai v0.0.0-20260102002309-90abcfc27670 diff --git a/pkg/termite/go.sum b/pkg/termite/go.sum index 49d7c9c..740421e 100644 --- a/pkg/termite/go.sum +++ b/pkg/termite/go.sum @@ -1,6 +1,4 @@ github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= -github.com/ajroetker/hugot v0.0.0-20260107061453-f83ae7b7c356 h1:opUqf468UDMKDFEmmQZybIi5s7egT0RZGZX+WeylE8A= -github.com/ajroetker/hugot v0.0.0-20260107061453-f83ae7b7c356/go.mod h1:I3/sobJjSQecs1xBUKnbjBKcbaX03uQ7gfkDW55zAU8= github.com/ajroetker/ortgenai v0.0.0-20260102002309-90abcfc27670 h1:bT8mAdiBPSwQz2O4aPbbfZgj8JQZiqTnBZ+FPqN/TTI= github.com/ajroetker/ortgenai v0.0.0-20260102002309-90abcfc27670/go.mod h1:zolhST8PioaCCsnwHBUesIBKU4CW/ZrF169YjFfg7BY= github.com/antflydb/antfly-go/libaf v0.0.0-20260105234922-f6ed1eb6788d h1:7C9y7i5pb6frXFtsXszDLVhevEQ8/A/ySDwLHrq4dog= @@ -229,6 +227,8 @@ github.com/sugarme/tokenizer v0.3.0 h1:FE8DYbNSz/kSbgEo9l/RjgYHkIJYEdskumitFQBE9 github.com/sugarme/tokenizer v0.3.0/go.mod h1:VJ+DLK5ZEZwzvODOWwY0cw+B1dabTd3nCB5HuFCItCc= github.com/timkaye11/gomlx v0.0.0-20251221044952-ee84bc4bbaf7 h1:9WfUFJSnLpAlSEWUUtucZrDSDZmE60H0xQPHohksQu8= github.com/timkaye11/gomlx v0.0.0-20251221044952-ee84bc4bbaf7/go.mod h1:HKSGc4Btb3VntH/BV/UsJYzgTMbLT/Gk69kv95rjruM= +github.com/timkaye11/hugot v0.0.0-20260108205439-6127b8a4c499 h1:f5+W2JKwNwLLkYzZ+A+Wcv9shdFSffo1L8zasfaIUAw= +github.com/timkaye11/hugot v0.0.0-20260108205439-6127b8a4c499/go.mod h1:F8q/Zn0G8jUFUCp6KAuUvQRRsHZk3VEb0Efo0c6z/N8= github.com/timkaye11/onnx-gomlx v0.0.0-20251217194823-a871bb8cc687 h1:mPN2mhB+q1DwpMnJCdmG79oFtohAgomHfw45pdxurys= github.com/timkaye11/onnx-gomlx v0.0.0-20251217194823-a871bb8cc687/go.mod h1:V0xRbk2eozhH6FCx3RmyjNcfi7AujLP+mmaOHOpr65s= github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=