Skip to content

Commit a3054a2

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 1dc70fc commit a3054a2

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
@@ -124,6 +124,25 @@ var _ = Describe("grpcModelOpts NBatch", func() {
124124
Expect(opts.NBatch).To(BeEquivalentTo(1024))
125125
})
126126

127+
It("sizes the batch to the context window for embedding models", func() {
128+
// Embedding/rerank pool over the whole sequence in one physical batch
129+
// (n_ubatch); without this the input is capped at the 512 default and
130+
// the backend returns "input is too large to process".
131+
embeddings := true
132+
cfg := config.ModelConfig{Threads: &threads, LLMConfig: config.LLMConfig{ContextSize: &ctx}}
133+
cfg.Embeddings = &embeddings
134+
opts := grpcModelOpts(cfg, "/tmp/models")
135+
Expect(opts.NBatch).To(BeEquivalentTo(4096))
136+
})
137+
138+
It("sizes the batch to the context window for rerank models", func() {
139+
reranking := true
140+
cfg := config.ModelConfig{Threads: &threads, LLMConfig: config.LLMConfig{ContextSize: &ctx}}
141+
cfg.Reranking = &reranking
142+
opts := grpcModelOpts(cfg, "/tmp/models")
143+
Expect(opts.NBatch).To(BeEquivalentTo(4096))
144+
})
145+
127146
It("does not raise the batch when a score model's context is below the default", func() {
128147
small := 256
129148
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)