perf(retain): give chunks extraction mode its own sub-batch budget - #3805
Draft
nicoloboschi wants to merge 2 commits into
Draft
perf(retain): give chunks extraction mode its own sub-batch budget#3805nicoloboschi wants to merge 2 commits into
nicoloboschi wants to merge 2 commits into
Conversation
retain_batch_tokens (10k) is sized to keep a fact-extraction prompt inside a
context window. retain_extraction_mode="chunks" never builds that prompt — it
stores each chunk as-is — so the same ceiling bounded nothing real there and
instead capped the streaming pipeline at ~13-29 chunks per sequential pass.
That serialised a per-chunk fan-out built to run wide, and made
retain_chunk_batch_size (100) unreachable: the sub-batch slice, not the config,
was always what ended a consumer batch.
Add HINDSIGHT_API_RETAIN_CHUNKS_MODE_BATCH_TOKENS (default 500_000, ~2MB of
text) and use it when the resolved mode is "chunks". The bound that still
matters in that mode is memory, and that is what the new default is sized for.
_resolve_retain_config now applies the provider == "none" chunks-mode override
too. It advertises itself as a mirror of what _retain_batch_async_internal
resolves, and without that half a server with no LLM reported whatever mode the
bank had on paper while every sub-batch actually ran in chunks mode.
Measured in-process, 2MB document, local embeddings, 3 repeats each:
old budget new budget
wall 32.2s 19.8s
throughput 26.4 ch/s 43.0 ch/s
streaming passes 32 1
embed backend requests 32 14
peak embed in-flight 1 4
DB commits 32 (max 29) 9 (max 100)
peak memory 10.6MB 17.3MB
At 4MB: 64 passes -> 2, 16.8 -> 36.4 chunks/s. Stored output is byte-identical
across both budgets (same chunk count, indices and text per index).
Claude-Session: https://claude.ai/code/session_01HVZ94d143NPSsZjbnvwqba
…chmark
Two gaps found reviewing the previous commit.
_resolve_retain_config's provider == "none" override had no test, and it is the
exact thing whose absence made the chunks-mode budget a silent no-op on a server
with no LLM: the mode check read the bank's paper value and split at the LLM
budget anyway. Verified the new test fails on the unfixed method
('concise' != 'chunks') rather than passing for its own reasons.
The benchmark measures through two monkeypatches onto module attributes. If
either function is renamed or stops being the path retain takes, the patch stops
firing and the run reports a flattering zero. It now refuses to publish numbers
it never measured.
Claude-Session: https://claude.ai/code/session_01HVZ94d143NPSsZjbnvwqba
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.
The problem
retain_batch_tokens(10k) exists to keep a fact-extraction prompt inside a context window.retain_extraction_mode="chunks"never builds that prompt — it stores each chunk as-is and calls no LLM — but it was still cut into sub-batches by the same ceiling.The consequences compound:
retain_chunk_size, and sub-batches run sequentially (_run_retain_execution), each with its own document tracking, recovery chunk-hash scan and final ANN pass.CoalescingEmbedderadded in fix(retain): batch a document's chunk embeddings instead of one request per chunk #3785 never had more than one backend request in flight.retain_chunk_batch_size(default 100) was unreachable: the sub-batch slice, not the config, ended every consumer batch.Follow-up to the ceiling flagged but not fixed in #3784 / #3785.
The change
New static config
retain_chunks_mode_batch_tokens/HINDSIGHT_API_RETAIN_CHUNKS_MODE_BATCH_TOKENS, default500_000(~2MB of text), selected bysub_batch_token_budget()once the retain config is resolved. Every LLM mode keeps the 10k budget unchanged. What still needs bounding in chunks mode is memory, and that is what the new default is sized for._resolve_retain_confignow also applies theprovider == "none"→ chunks-mode override. It advertises itself as a mirror of what_retain_batch_async_internalresolves, and without that half a server with no LLM reported whatever mode the bank had on paper while every sub-batch actually ran in chunks mode — which made this whole change a silent no-op on exactly the deployment it was written for.Deliberately not changed:
_split_contents_into_async_children. That site packs distinct documents into child operations and never fragments a single item, so a larger budget there would reduce cross-worker parallelism rather than increase it.Measurements
In-process retain,
LLM_PROVIDER=none(forces chunks mode), local embeddings, isolated pg0, 2MB document, 3 repeats per arm. The structural columns were identical on every repeat; only wall time varied (±0.6s).At 4MB: 64 passes → 2, 16.8 → 36.4 chunks/s.
peak embed in-flight = 1is the headline: the fan-out existed, but a pass never held enough chunks for a second request to overlap.The
db_commitsrow is what settles whetherretain_chunk_batch_sizeis dead config —max 29against a configured 100 means it never bound once. After the changemax 100: it becomes the real bound. It is dormant, not dead; it should not be removed.Behaviour is unchanged
The same document through both budgets produces byte-identical stored state — 427 chunks, contiguous
chunk_index, identical text per index (same SHA-256 over theindex:textsequence).Tests
hindsight-api-slim/tests/test_chunks_mode_batch_tokens.py(9 tests, no LLM, no DB):retain_chunk_batch_sizeunder the new budget but not the old one, with the same total chunk count either way;provider == "none"override, verified to fail on the unfixed method ('concise' != 'chunks') rather than passing for its own reasons.hindsight-dev/benchmarks/perf/chunks_mode_parallelism.pyreproduces the table above (passes / embed requests / peak in-flight / DB commits / tracemalloc peak). It measures through two monkeypatches, so it refuses to print numbers it never actually recorded.What has not been verified
The DB-backed retain suites were not run locally — they need a real LLM provider and the benchmark worktree pins
LLM_PROVIDER=none. CI covers them. Locallint.sheslint could not run in a fresh worktree (nonode_modules);ruff,ruff format,tyand the touched test suites all pass, and no TypeScript is in the diff.The gains were measured with local embeddings, where the win comes from feeding the model larger batches. With a remote backend (TEI/OpenAI) the 32 → 14 request drop is HTTP round-trips, so the improvement should be larger.
https://claude.ai/code/session_01HVZ94d143NPSsZjbnvwqba