Skip to content

Commit 8290c3f

Browse files
committed
feat(backend): auto-size batch to context for embedding and rerank models
Embedding and rerank models pool over the whole input in a single physical batch (n_ubatch). With batch left at the 512 default, the backend rejects longer inputs with "input is too large to process", silently capping a large-context embedder (e.g. 8k/32k) at 512 tokens. Size n_batch to the context for these single-pass usecases, mirroring the existing FLAG_SCORE behaviour; an explicit batch: still wins. Extracts EffectiveContextSize/EffectiveBatchSize from grpcModelOpts so the effective decode window has one home for other callers to reuse. Adds an e2e-aio regression test that embeds a >512-token input. The AIO embedding model is switched to nomic-embed-text-v1.5 (2048 context) because the previous granite model was capped at 512 tokens and could not exercise the larger batch. Assisted-by: claude-code:claude-opus-4-8 [Claude Code] Signed-off-by: Richard Palethorpe <io@richiejp.com>
1 parent 530e047 commit 8290c3f

4 files changed

Lines changed: 94 additions & 15 deletions

File tree

core/backend/options.go

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -87,25 +87,47 @@ func getSeed(c config.ModelConfig) int32 {
8787
return seed
8888
}
8989

90-
func grpcModelOpts(c config.ModelConfig, modelPath string) *pb.ModelOptions {
91-
// Resolve context size first — the backend defaults to 4096 when unset,
92-
// and batch sizing below has to match that effective value or the
93-
// FLAG_SCORE guard misses the n_batch < n_ctx GGML_ASSERT crash.
94-
ctxSize := 4096
90+
// DefaultContextSize and DefaultBatchSize are the backend's fallbacks when a
91+
// model config leaves them unset. Exported so callers that must respect the
92+
// effective decode window — notably the router's prompt trimmer — resolve the
93+
// same numbers grpcModelOpts does instead of guessing.
94+
const (
95+
DefaultContextSize = 4096
96+
DefaultBatchSize = 512
97+
)
98+
99+
// EffectiveContextSize is the context window the backend will run with: the
100+
// configured value, or DefaultContextSize when unset.
101+
func EffectiveContextSize(c config.ModelConfig) int {
95102
if c.ContextSize != nil {
96-
ctxSize = *c.ContextSize
103+
return *c.ContextSize
97104
}
105+
return DefaultContextSize
106+
}
98107

99-
b := 512
108+
// EffectiveBatchSize is the single-decode batch the backend will run with.
109+
// Score, embedding and rerank all process the whole input in one pass: score
110+
// decodes prompt+candidate (asserts n_tokens <= n_batch), and embedding/rerank
111+
// pool over the full sequence in one physical batch (n_ubatch). So the batch
112+
// is sized to the context — anything that fits the context fits one pass,
113+
// avoiding both the GGML_ASSERT crash and the "input is too large to process"
114+
// error. Explicit `batch:` always wins.
115+
func EffectiveBatchSize(c config.ModelConfig) int {
100116
if c.Batch != 0 {
101-
b = c.Batch
102-
} else if c.HasUsecases(config.FLAG_SCORE) && ctxSize > b {
103-
// Score models decode prompt+candidate in one llama_decode which
104-
// asserts n_tokens <= n_batch and aborts on failure. Sizing the
105-
// batch to n_ctx means anything that fits the context fits one
106-
// decode. Explicit `batch:` in the config still wins.
107-
b = ctxSize
117+
return c.Batch
108118
}
119+
singlePass := c.HasUsecases(config.FLAG_SCORE) ||
120+
c.HasUsecases(config.FLAG_EMBEDDINGS) ||
121+
c.HasUsecases(config.FLAG_RERANK)
122+
if ctx := EffectiveContextSize(c); singlePass && ctx > DefaultBatchSize {
123+
return ctx
124+
}
125+
return DefaultBatchSize
126+
}
127+
128+
func grpcModelOpts(c config.ModelConfig, modelPath string) *pb.ModelOptions {
129+
ctxSize := EffectiveContextSize(c)
130+
b := EffectiveBatchSize(c)
109131

110132
flashAttention := "auto"
111133

core/backend/options_internal_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,25 @@ var _ = Describe("grpcModelOpts NBatch", func() {
102102
Expect(opts.NBatch).To(BeEquivalentTo(1024))
103103
})
104104

105+
It("sizes the batch to the context window for embedding models", func() {
106+
// Embedding/rerank pool over the whole sequence in one physical batch
107+
// (n_ubatch); without this the input is capped at the 512 default and
108+
// the backend returns "input is too large to process".
109+
embeddings := true
110+
cfg := config.ModelConfig{Threads: &threads, LLMConfig: config.LLMConfig{ContextSize: &ctx}}
111+
cfg.Embeddings = &embeddings
112+
opts := grpcModelOpts(cfg, "/tmp/models")
113+
Expect(opts.NBatch).To(BeEquivalentTo(4096))
114+
})
115+
116+
It("sizes the batch to the context window for rerank models", func() {
117+
reranking := true
118+
cfg := config.ModelConfig{Threads: &threads, LLMConfig: config.LLMConfig{ContextSize: &ctx}}
119+
cfg.Reranking = &reranking
120+
opts := grpcModelOpts(cfg, "/tmp/models")
121+
Expect(opts.NBatch).To(BeEquivalentTo(4096))
122+
})
123+
105124
It("does not raise the batch when a score model's context is below the default", func() {
106125
small := 256
107126
cfg := config.ModelConfig{Threads: &threads, LLMConfig: config.LLMConfig{ContextSize: &small}, KnownUsecases: &scoreUsecase}

tests/e2e-aio/e2e_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,37 @@ var _ = Describe("E2E test", func() {
222222
Expect(resp3.Data[1].Embedding).To(Equal(resp2.Data[0].Embedding))
223223
Expect(resp3.Data[0].Embedding).ToNot(Equal(resp3.Data[1].Embedding))
224224
})
225+
226+
// Regression guard for the auto-batch fix (core/backend/options.go
227+
// EffectiveBatchSize). Embeddings pool over the whole sequence in a
228+
// single physical batch (n_ubatch == n_batch), so an input longer
229+
// than n_batch is rejected by the backend with "input is too large
230+
// to process". Before the fix n_batch defaulted to 512 regardless of
231+
// the model's context, so any prompt over ~512 tokens failed here.
232+
// The embedding model is configured with a 2048 context (see
233+
// models/embeddings.yaml); this input is comfortably over 512 tokens
234+
// and under that context, so it must embed in one pass.
235+
It("embeds an input larger than the default 512 batch", func() {
236+
var b bytes.Buffer
237+
// ~100 short sentences ≈ 1000+ tokens: well past the old 512
238+
// batch ceiling, well within the 2048 context.
239+
for i := range 100 {
240+
fmt.Fprintf(&b, "This is sentence number %d discussing organic skincare and machine learning. ", i)
241+
}
242+
longInput := b.String()
243+
244+
resp, err := client.Embeddings.New(context.TODO(),
245+
openai.EmbeddingNewParams{
246+
Input: openai.EmbeddingNewParamsInputUnion{
247+
OfArrayOfStrings: []string{longInput},
248+
},
249+
Model: openai.EmbeddingModelTextEmbeddingAda002,
250+
},
251+
)
252+
Expect(err).ToNot(HaveOccurred(), "a >512-token input must embed in a single batch (auto-batch sizing)")
253+
Expect(len(resp.Data)).To(Equal(1), fmt.Sprint(resp))
254+
Expect(resp.Data[0].Embedding).ToNot(BeEmpty())
255+
})
225256
})
226257

227258
Context("vision", func() {
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
embeddings: true
22
name: text-embedding-ada-002
33
backend: llama-cpp
4+
# nomic-embed-text-v1.5 has a 2048-token context, unlike the previous 512-token
5+
# granite model. The larger context is what makes the long-input embedding test
6+
# (e2e_test.go) meaningful: it exercises the auto-batch fix where n_batch is
7+
# sized up to the context window (core/backend/options.go EffectiveBatchSize) so
8+
# a >512-token input embeds in a single pass instead of failing with "input is
9+
# too large to process" against the default 512 batch.
10+
context_size: 2048
411
parameters:
5-
model: huggingface://bartowski/granite-embedding-107m-multilingual-GGUF/granite-embedding-107m-multilingual-f16.gguf
12+
model: huggingface://nomic-ai/nomic-embed-text-v1.5-GGUF/nomic-embed-text-v1.5.f16.gguf

0 commit comments

Comments
 (0)