indexer: order-preserving concurrent embedding requests (#107) - #117
Merged
IceRhymers merged 1 commit intoJul 25, 2026
Merged
Conversation
Batches now dispatch through a ThreadPoolExecutor.map (never as_completed, which yields in completion order), so vectors always return in submission order regardless of which request finishes first -- the whole point of the issue, since _precompute_chunk_writer re-slices the flat result positionally and a reorder would silently corrupt embeddings for the wrong file. - app/embed.py: databricks_embedder gains concurrency (default 1, a deliberate divergence from this file's default-mirroring convention so the concurrent path is opt-in only at the indexer call site); _query_batch gains ordinal/offset so a count mismatch names the offending batch (AC2). Existing retry semantics and the SDK's own 429/Retry-After handling are unchanged -- no third retry layer added. - app/config.py: semantic_embedding_concurrency = 4 (workers x concurrency = 8 in-flight by default, 16 at the config.yaml le=8 ceiling, both under the SDK's 20-connection pool). - indexer/repo_config.py: SemanticOverrides.embedding_concurrency (1..8) overlays the setting from config.yaml, the job's only reachable config surface. - scripts/measure_embedding_concurrency.py: offline scaling harness (no network); measured ~1x/2x/4x/8x at concurrency 1/2/4/8. - Docs: config.yaml, semantic-enablement.md, indexing-parallelism.md, app/AGENTS.md, indexer/AGENTS.md. - Tests: 24 cases in test_embed.py (submission-order proof plus a non-vacuity companion, AST-based as_completed tripwire, a structural queued-batch-cancellation bound, deterministic lowest-ordinal-failure reporting, the concurrency clamp's degenerate paths) and 3 sites in test_repo_config.py for the new config field. indexer/job.py is unchanged: it still calls embed_fn(all_texts) once and re-slices positionally. No INDEX_SEMANTICS_VERSION bump -- this changes dispatch mechanics, not extraction output. Refs #107
IceRhymers
force-pushed
the
feat/107-concurrent-embeddings
branch
from
July 25, 2026 18:18
6765e7c to
045bdaf
Compare
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements issue #107: the indexer's
app/embed.py:databricks_embedder.embed()now dispatches batches concurrently instead of one HTTP call at a time, while guaranteeing vectors return in submission order regardless of completion order — the load-bearing property, sinceindexer/job.py:_precompute_chunk_writerre-slices the flat result positionally and a silent reorder would attach the wrong file's embeddings with no error.Executed from the critic-approved plan at
.omc/plans/issue-107/approved-plan.md(APPROVE after 4 review rounds + a base-revalidation pass against732a7d7).Correction to a stale figure carried in epic #110 / the issue body: the "30k-chunk cap" / "~470 sequential requests" language there is stale — the real cap is
semantic_max_chunks_per_repo = 8000(app/config.py), i.e. ~125 batches of 64 at the default batch size. The direction of the argument is unaffected.What changed
app/embed.py—databricks_embeddergainsconcurrency: int = 1. Whenworkers = max(1, min(concurrency, len(batches))) > 1, batches dispatch viaThreadPoolExecutor(max_workers=workers).map(...)— neveras_completed, which yields in completion order and would defeat the whole point.concurrency <= 1, or a text list short enough to produce one batch (e.g. every query-time call), constructs no pool and spawns no thread — a strict no-op on that path.concurrencydeliberately does not mirror this file's usual default-mirroring convention (every other param matches itsSettingstwin): the concurrent path is opt-in only at the call site that knows it's the indexer (get_embedder)._query_batchgains keyword-onlyordinal/offsetso a count mismatch names the offending batch (AC2), e.g.embedder returned 63 vectors for 64 texts (batch 3, texts[192:256]). Both existing message-matching tests pass unmodified (match=isre.search, substring preserved).databricks-sdk(0.122.0) already absorbs AI Gateway 429/503s viaRetry-After-honoring backoff (_RetryAfterCustomizer, defaulting to 1s without the header) before_query_batch's own bounded retry ever sees them. No third retry layer added;_query_batch's retry shape andmax_retriesdefault are untouched. The one gap this creates — that absorption is invisible at the job's INFO log level (the SDK logs at DEBUG) — is documented in the runbook as a single-named-logger operator step (databricks.sdk.retries), never a code change (two existing tripwires intest_job_redaction.pyguard against exactly that landing inindexer/*.py).app/config.py—semantic_embedding_concurrency: int = 4. Total in-flight gateway requests iseffective_workers x concurrency: 2 x 4 = 8 at the default, 2 x 8 = 16 at the config.yamlle=8ceiling, both under the SDK's 20-connection pool (pool_block=True, so exceeding it silently serializes rather than erroring — staying under 20 is load-bearing). TheSettingsfield itself is intentionally unbounded, mirroring itssemantic_embedding_batch_sizesibling — thele=8bound lives on theconfig.yamloverlay, withdatabricks_embedder's own clamp as the real safety net.indexer/repo_config.py—SemanticOverrides.embedding_concurrency: int | None = Field(ge=1, le=8), wired intosettings_overrides(), class docstring updated.config.yaml,docs/runbooks/semantic-enablement.md(§4, §6),docs/runbooks/indexing-parallelism.md(§3),app/AGENTS.md,indexer/AGENTS.md— documentation updated with the in-flight/memory arithmetic, the rollback switch (concurrency: 1), and the 429/DEBUG-logger posture.scripts/measure_embedding_concurrency.py(new) — offline harness (fake client, fixed sleep, no network, not run in CI) measuring dispatch scaling. Output below.indexer/job.py(zero changes — still callsembed_fn(all_texts)once and re-slices positionally),indexer/timing.py(no context propagation into embed threads — deliberately rejected after prototypingcopy_context(); see the plan's round-2 finding 3 —PhaseTimeris documented not-thread-safe and its cross-thread-isolation docstring, which names indexer: concurrent embedding requests, order-preserving #107, stays true),INDEX_SEMANTICS_VERSION(this changes dispatch mechanics, not extraction output;app/embed.py/app/config.pyare deliberately outside the semantics-version tripwire's watched paths).Tests
24 cases in
tests/unit/test_embed.py(13 planned + defensive additions from an independent code-review pass — see below) plus 3 sites intests/unit/test_repo_config.py. Highlights:test_concurrent_batches_return_in_submission_order) with a non-vacuity companion proving the fixture's completion order genuinely differs from submission order.app.embednever referencesas_completed, including via an aliased import.test_failure_cancels_queued_batches) — every non-failing batch parks on anEventthat is never set, sostartedis fixed by the code path, not by timing. Verified stable across 40+ repeated runs, including under 4x CPU oversubscription.concurrency <= 0, 0 batches, 1 batch) all route to the serial no-pool path.concurrencydefaults to 1) and a non-degenerate-batch_sizetest for theoffsetarithmetic — both added after an independentcode-reviewerpass found the original fixtures could pass with either mutated.Independent code review
Ran a fresh
code-revieweragent in a separate context. Verdict: no CRITICAL/HIGH. Found 2 MEDIUM (both test-coverage gaps, both mutation-proven — fixed: a direct assertion pinning the default to 1, and a non-degenerate-batch_sizetest for theoffsetmath) and several LOW (fixed: closed an aliased-import gap in the AST tripwire; clarified runbook wording that thele=8bound is config.yaml-only, not the env surface; documented the bounded failure-path latency increase under concurrency). Two LOW suggestions were not applied because they'd contradict explicit, already-adversarially-reviewed design decisions in the approved plan: making_query_batch'sordinal/offsetrequired (the plan explicitly keeps them defaulted so the function stays callable in isolation) and bounding theSettingsfield itself (deliberately unbounded, mirroring itsbatch_size/timeout_ssiblings, with the real clamp insidedatabricks_embedder).Measurement (AC3)
Part 1 — scaling shape (committed, deterministic):
Parts 2 & 3 — real-repo
embed=timing comparison and thedatabricks.sdk.retriesDEBUG-logger 429 check require a live dev workspace. Not run in this environment — no workspace access available. Deferred with this named reason, per the plan's honesty requirement; the scaling-shape result plus the offered-load ceiling (§ above) is the evidence available at this time.Gates
make lint(ruff check + format + mypy overapp indexer webui): clean.make test(pytest -m "unit or observability"): 1238 passed, 1 failed. The one failure —tests/unit/test_semantics_version_tripwire.py::test_semantics_change_bumps_the_index_semantics_version— is an inherited failure, present on the unmodified base branch (origin/integration/indexer-performance@732a7d7) too: an earlier merged PR (indexer: single-pass in-memory tarball ingestion (drop extract-to-disk) #106) addedindexer/ingest.pywithout a version bump. Verified identical on both branches; not this PR's to fix, and the tripwire's own docstring forbids silencing it. No other test fails.make test-integration(pytest -m "integration or e2e", against the localcodesearch-pgcontainer): 8 failed / 205 passed / 43 errors, identical count with and without this diff. Cause: the local Postgres container lacks thelakebase_tokenizer/lakebase_vectorextensions real Lakebase provides — a pre-existing environment gap, not related to this change. No integration test touchesapp/embed.py.Closure ownership
Refs #107(notCloses) — this is an arc child of epic #110; #107 closes when the umbrella PR #111 merges tomaster(or is closed manually alongside its siblings), not automatically from this PR.Test plan
make lintcleanmake test— green except the one named inherited failuremake test-integration— run locally; failure signature identical to the unmodified basecode-reviewerpass; all actionable findings fixedembed=timing comparison and DEBUG-logger 429 check (parts 2–3 of AC3) — deferred, no live workspace available in this environment