Skip to content

[Router] vectorstore: bound ingestion memory with streaming batches - #2666

Open
Peterren wants to merge 1 commit into
vllm-project:mainfrom
Peterren:feat/vectorstore-bounded-batches
Open

[Router] vectorstore: bound ingestion memory with streaming batches#2666
Peterren wants to merge 1 commit into
vllm-project:mainfrom
Peterren:feat/vectorstore-bounded-batches

Conversation

@Peterren

Copy link
Copy Markdown
Contributor

Part of #2474 (vector-store ingestion hardening) — implements the "bounded streaming batches" acceptance item. Not a full close of the umbrella issue, whose remaining slices (status retention, saga reconciliation, parser streaming) are tracked separately.

Purpose

  • What: The ingestion pipeline embedded every chunk of a file and then inserted them all in a single InsertChunks call. That holds the whole file's chunk text and embedding vectors in memory simultaneously — an unbounded-by-count per-job memory spike, multiplied across ingestion workers. This PR embeds and stores chunks in fixed-size windows instead (default 64, configurable via vector_store.ingestion_batch_size), bounding peak per-job memory to roughly O(batch_size × embedding_dimension) regardless of file size.
  • Why: Large uploads with high-dimensional embeddings could drive large transient allocations; batching makes ingestion memory predictable and bounded. This implements the bounded streaming batches acceptance item of the vector-store hardening issue vectorstore: make ingestion cancellable, bounded, and transactional #2474.
  • Module(s): Router (primary), with config-surface updates to Dashboard and E2E/Helm values.

Behavior notes

  • ctx is checked before each batch, so a cancelled lifecycle aborts promptly between batches. Chunk IDs remain globally correct across batch boundaries (indexing is unchanged).
  • Batching changes failure semantics: earlier batches are already inserted when a later one fails. To preserve the prior all-or-nothing invariant (a failed file leaves nothing searchable), a partial failure now best-effort removes the already-inserted chunks via DeleteByFileID, detached from the (possibly cancelled) job context so cleanup still runs during shutdown, but bounded so a wedged backend cannot block the worker.
  • Out of scope (tracked separately under vectorstore: make ingestion cancellable, bounded, and transactional #2474): streaming the parser (text extraction still returns the full document), status TTL/retention, and full transactional/saga reconciliation.

Test Plan

From src/semantic-router:

go build ./pkg/...
go build -tags=milvus ./cmd            # real router binary
go test -race ./pkg/vectorstore ./pkg/routerruntime
go test ./pkg/config
golangci-lint run ./pkg/vectorstore/... ./pkg/config/... ./pkg/routerruntime/... \
  --config ../../tools/linter/go/.golangci.yml

New unit tests in pkg/vectorstore/pipeline_batch_test.go:

  • Batch-boundary correctness (table): chunk counts of 1/3/5/8/13 against batch sizes 1/4/5 — asserts every chunk is stored, the number of InsertChunks calls equals ceil(N/batch), and no batch exceeds the configured size.
  • Default batch size: unset → 64 (65 chunks → 2 inserts).
  • Partial-failure cleanup: a failure in a later batch removes the already-inserted chunks; the store ends with 0 searchable chunks and the file counted as failed.
  • First-batch failure: no insert happened, so no cleanup is attempted.

Test Result

  • All of the above pass. go test -race is clean for pkg/vectorstore, pkg/routerruntime, and pkg/config; golangci-lint reports 0 issues; gofmt/go vet clean; the router binary builds with the milvus tag.
  • The Kubernetes-backed rag-hybrid-search E2E profile is not run locally (requires a cluster); it exercises this path in CI.
  • Follow-up: the remaining vectorstore: make ingestion cancellable, bounded, and transactional #2474 items (parser streaming, status retention, saga reconciliation) are separate slices.

Semantic Router PR Checklist
  • PR title uses module-aligned prefixes such as [Router], [CLI], [Dashboard], [Operator], [Fleet-Sim], [Bindings], [Training], [E2E], [Docs], or [CI/Build]
  • If the PR spans multiple modules, the title includes all relevant prefixes
  • Commits in this PR are signed off with git commit -s
  • The Purpose, Test Plan, and Test Result sections reflect the actual scope, commands, and blockers for this change

Previously processJob embedded every chunk of a file and then inserted them
all in a single call, holding the whole file's chunk text and embedding
vectors in memory at once. For large files with high-dimensional embeddings
this is an unbounded-by-count per-job memory spike, multiplied across workers.

Embed and store chunks in fixed-size windows instead (default 64, configurable
via vector_store.ingestion_batch_size). This bounds peak per-job memory to
roughly O(batch_size x embedding_dimension) regardless of file size. ctx is
checked before each batch so a cancelled lifecycle aborts promptly between
batches. Chunk IDs remain globally correct across batch boundaries.

Batching changes failure semantics: earlier batches are already inserted when
a later one fails. To preserve the prior all-or-nothing invariant (a failed
file leaves nothing searchable), a partial failure now best-effort removes the
already-inserted chunks via DeleteByFileID, detached from the job context so
cleanup still runs during shutdown but bounded so a wedged backend cannot block
the worker. Full transactional reconciliation of ingestion remains tracked
separately under the vector-store hardening issue.

Adds tests for batch-boundary correctness across several chunk/batch sizes,
the configurable default, partial-failure cleanup, and the first-batch-failure
no-op cleanup path. go test -race is clean for pkg/vectorstore,
pkg/routerruntime, and pkg/config.

Signed-off-by: Yincheng Ren <27608815+Peterren@users.noreply.github.com>
@Peterren
Peterren requested review from Xunzhuo and rootfs as code owners July 25, 2026 04:37
@netlify

netlify Bot commented Jul 25, 2026

Copy link
Copy Markdown

Deploy Preview for vllm-semantic-router ready!

Name Link
🔨 Latest commit 169fb47
🔍 Latest deploy log https://app.netlify.com/projects/vllm-semantic-router/deploys/6a643daabef9c80008e0f9e2
😎 Deploy Preview https://deploy-preview-2666--vllm-semantic-router.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions

Copy link
Copy Markdown
Contributor

👥 vLLM Semantic Team Notification

The following members have been identified for the changed files in this PR and have been automatically assigned when their GitHub accounts are assignable in this repository:

📁 config

Owners: @FAUST-BENCHOU, @shraderdm, @drivebyer, @ramkrishs, @WUKUNTAI-0211, @AayushSaini101, @siloteemu
Files changed:

  • config/config.yaml

📁 dashboard

Owners: @FAUST-BENCHOU, @shraderdm, @drivebyer, @ramkrishs, @WUKUNTAI-0211, @AayushSaini101, @siloteemu
Files changed:

  • dashboard/frontend/src/pages/configPageRouterDefaultsCatalog.ts
  • dashboard/frontend/src/pages/configPageRouterDefaultsSupport.ts
  • dashboard/frontend/src/pages/configPageSupport.ts

📁 deploy

Owners: @FAUST-BENCHOU, @shraderdm, @drivebyer, @ramkrishs, @WUKUNTAI-0211, @AayushSaini101, @siloteemu
Files changed:

  • deploy/helm/semantic-router/values.yaml

📁 e2e

Owners: @FAUST-BENCHOU, @shraderdm, @drivebyer, @ramkrishs, @WUKUNTAI-0211, @AayushSaini101, @siloteemu
Files changed:

  • e2e/profiles/rag-hybrid-search/values.yaml

📁 src/semantic-router

Owners: @FAUST-BENCHOU, @shraderdm, @drivebyer, @ramkrishs, @WUKUNTAI-0211, @AayushSaini101, @siloteemu
Files changed:

  • src/semantic-router/pkg/config/vectorstore.go
  • src/semantic-router/pkg/routerruntime/vectorstore_runtime.go
  • src/semantic-router/pkg/vectorstore/pipeline.go
  • src/semantic-router/pkg/vectorstore/pipeline_batch_test.go

vLLM Semantic Router

🎉 Thanks for your contributions!

This comment was automatically generated based on the OWNER files in the repository.

@github-actions

Copy link
Copy Markdown
Contributor

✅ Supply Chain Security Report — All Clear

Scanner Status Findings
AST Codebase Scan (Py, Go, JS/TS, Rust) 29 finding(s) — MEDIUM: 22 · LOW: 7
AST PR Diff Scan No issues detected
Regex Fallback Scan No issues detected

Scanned at 2026-07-25T05:02:13.877Z · View full workflow logs

@github-actions github-actions Bot deleted a comment from codecov-commenter Jul 25, 2026
}
cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), batchCleanupTimeout)
defer cancel()
_ = p.backend.DeleteByFileID(cleanupCtx, job.VectorStoreID, job.FileID)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 on keeping the all or nothing invariant, nice touch. One thing I've been bitten by before with compensation deletes keyed on file ID: AttachFile doesn't dedupe, so a retry of the same file can run while the old attachment is still completed. If the retry fails here, this delete wipes the previous attachment's chunks too, and it still shows completed with nothing searchable behind it. Tracking the chunk IDs this job inserted and deleting only those would avoid that.

// whose ingestion failed partway through. The delete is detached from the
// job context (which may be cancelled during shutdown) but bounded by
// batchCleanupTimeout so a wedged backend cannot block the worker. Failure to
// clean up is logged implicitly by leaving the file marked failed; it is not

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just thinking out loud here: If DeleteByFileID itself fails or times out, the stated invariant (failed file leaves nothing searchable) is broken with zero operator signal. Can we log a warning with the store and file IDs (and ideally a metric) when cleanup fails?

@drivebyer

drivebyer commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Nice work bounding the embed/insert memory! A few things worth a look:

  • MemoryBackend.InsertChunks rebuilds the full text indexes on every call (memory_backend.go:120); batching turns one rebuild per file into ⌈N/64⌉ growing ones. Moving the rebuild out of the per-batch path would avoid this.
  • MilvusBackend runs a synchronous Flush per call (milvus_backend.go:238), so a 10k-chunk file flushes ~157 times. Maybe flush once per file, and raise the default batch above 64?
  • I'd drop the ingestion_batch_size config: users have no good way to pick a value, the right one is backend-dependent, and without an upper bound in Validate a huge value silently restores single-insert behavior. A constant plus PipelineConfig.BatchSize seems enough ([Router] vectorstore: thread context into Embedder interface #2535 kept the drain timeout config-only).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants