Skip to content

refa: optimize wiki compilation concurrency#17192

Open
buua436 wants to merge 4 commits into
infiniflow:mainfrom
buua436:b116
Open

refa: optimize wiki compilation concurrency#17192
buua436 wants to merge 4 commits into
infiniflow:mainfrom
buua436:b116

Conversation

@buua436

@buua436 buua436 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Improve Wiki compilation throughput while keeping LLM concurrency and memory usage bounded.

  • Add shared LLM pool control with 20 active calls and 25 total pending/executing requests.
  • Allow MAP batches to feed the pool concurrently without unbounded queue growth.
  • Run Reduce LLM disambiguation batches concurrently.
  • Replace the full embedding similarity matrix with blockwise cosine computation.
  • Run REFINE pages concurrently through the shared LLM pool.

Type of change

  • Refactor (no functional change)

@buua436
buua436 requested a review from wangq8 July 21, 2026 11:02
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Embedding deduplication now uses bounded normalized cosine comparisons and concurrent ambiguity resolution. LLMCallPool adds pending-call backpressure, while wiki compilation shares bounded LLM concurrency across MAP, REDUCE, and REFINE phases.

Changes

Wiki compilation pipeline

Layer / File(s) Summary
Blockwise embedding deduplication
rag/advanced_rag/knowlege_compile/_common.py
Chunk processing uses indexed producer/worker execution; embeddings are compared in type-grouped blocks, and ambiguous decisions are resolved in concurrent waves before union-find updates.
LLM pool backpressure contract
rag/advanced_rag/knowlege_compile/structure.py
LLMCallPool accepts max_pending and exposes active and pending call counts while preserving concurrency and priority scheduling.
Bounded wiki compilation flow
rag/svr/task_executor_refactor/dataset_wiki_generator.py
MAP uses bounded producers, workers, queues, and shared pool admission; REDUCE and REFINE use pooled models, with updated aggregation and persistence wiring.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant run_wiki
  participant MAPQueue
  participant MAPWorker
  participant LLMCallPool
  participant wiki_map_from_chunks
  run_wiki->>MAPQueue: enqueue chunk batches
  MAPQueue->>MAPWorker: deliver batch jobs
  MAPWorker->>LLMCallPool: submit pooled MAP call
  LLMCallPool->>wiki_map_from_chunks: admit bounded LLM work
  wiki_map_from_chunks-->>MAPWorker: return batch results
  MAPWorker-->>run_wiki: aggregate document statistics
Loading

Possibly related PRs

Poem

A rabbit queued batches in a burrow so neat,
While pooled callers kept time with their feet.
Cosines hopped blockwise, pairs gathered in flight,
Union roots waited for decisions done right.
MAP, REDUCE, REFINE shared one bounded tune—
“Nibble the backlog beneath the moon!”

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: improving wiki compilation concurrency.
Description check ✅ Passed The description clearly states the problem, gives background context, and summarizes the main changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@buua436
buua436 marked this pull request as ready for review July 21, 2026 11:02
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. ☯️ refactor Pull request that refactor/refine code 🌈 python Pull requests that update Python code labels Jul 21, 2026
@buua436
buua436 marked this pull request as draft July 21, 2026 11:02
@buua436
buua436 marked this pull request as ready for review July 21, 2026 11:02
@yingfeng yingfeng added the ci Continue Integration label Jul 21, 2026

@wangq8 wangq8 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review: refa: optimize wiki compilation concurrency (#17192)

Thanks for this PR — the producer-consumer pattern and shared LLM pool are a solid architectural improvement. I left detailed notes below. The main callout is the concurrency explosion risk from max_workers=0 and the lost pre-filtering in disambiguation.

1. (Important) max_workers=0 can create unbounded concurrent tasks

In dataset_wiki_generator.py the MAP worker now calls wiki_map_from_chunks(..., max_workers=0). When max_workers is 0, _run_chunked_pipeline creates all sub-batches as concurrent asyncio tasks with no semaphore:

# _common.py, run_chunked_pipeline
semaphore = asyncio.Semaphore(max_workers) if max_workers and max_workers > 0 else None

If split_chunks produces e.g. 20 sub-batches per batch, and 20 workers are active, that is up to 400 asyncio tasks launched simultaneously. The shared LLMCallPool (max_pending=25) limits the actual LLM calls, but having hundreds of tasks pending is wasteful and risks excessive memory pressure from all the suspended coroutine frames.

Suggestion: Either set a reasonable non-zero max_workers (e.g. 6 like the default) or add an explicit bound inside _run_chunked_pipeline for the max_workers=0 path. At minimum, add a comment documenting the reasoning so future readers are not surprised.

2. Disambiguation pre-filtering is lost

Old code in _resolve_ambiguous_pairs:

batch = [(i, j) for i, j in batch if _root(i) != _root(j)]

This filtered pairs that were already redundant before firing the LLM call. The new code runs all batches concurrently via asyncio.gather and only filters at apply time. This means the LLM may be asked to judge pairs that will already be merged by an earlier-applied batch result.

This is a functional tradeoff (correctness is preserved by the final _root(i) != _root(j) guard in the apply loop), but it wastes LLM calls and tokens. Consider adding the pre-filtering back after gathering results, or at least batching the filtering with the concurrent dispatch.

3. Blockwise embedding dedup looks correct but worth verifying edge case

The _embedding_dedup change from sklearn.metrics.pairwise.cosine_similarity to blockwise matrix multiply is a good memory optimization. One thing to double-check in testing:

The DIY normalization:

norms = np.linalg.norm(matrix, axis=1, keepdims=True)
matrix = np.divide(matrix, norms, out=np.zeros_like(matrix), where=norms > 0)

The out=np.zeros_like(matrix) means zero-norm vectors become all-zero rows. Their cosine similarity with any other vector is then 0 (well below both thresholds). This is consistent with the old sklearn behavior (which would return NaN/0 for zero-vectors), but worth an explicit test case to confirm.

4. Minor: WIKI_MAP_QUEUE_SIZE = 5 may be too conservative

With 20 workers and potentially large documents, a queue depth of 5 means producers are throttled aggressively. If a document has many batches, the producer will put→block→put→block repeatedly, reducing throughput. Consider raising this to 2 * WIKI_MAP_LLM_POOL_SIZE (40) or similar, since the pool is the true concurrency limiter and the queue is just a buffer.

5. LLMCallPool pending gate is correct

The max_pending bound in LLMCallPool.call() gates on pending_count >= max_pending using async Condition.wait(). This correctly back-pressures without deadlocking since callers are pure async (not holding locks). ✅

6. Docs are solid

The docstring updates in _embedding_dedup, _resolve_ambiguous_pairs, and bulk_dedup_items clearly explain the changed semantics. The comments in dataset_wiki_generator.py about the producer-consumer design are helpful. ✅

Summary

The overall architecture is sound: shared pool bounding actual LLM calls, async queue for producer-consumer MAP, concurrent REFINE. My main concerns are (1) the unbounded task explosion from max_workers=0 and (2) the lost disambiguation pre-filtering. Neither is a correctness bug, but both could hurt production performance/latency.

Consider addressing #1 and #2 before merging. The rest looks good.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rag/advanced_rag/knowlege_compile/_common.py`:
- Around line 694-703: Optimize the blockwise comparison around the
upper-triangle branch by applying the ambiguous_low similarity mask to the
upper-triangle indices before iterating, so only relevant pairs reach the Python
loop while preserving existing merge and ambiguous classification. Update the
rows/cols iteration in this block to use zip(..., strict=True).
- Around line 787-790: Update the batch resolution flow around _resolve_batch
and asyncio.gather to use a local semaphore that limits concurrent batch tasks
to the available LLM capacity. Acquire the semaphore before creating or starting
each timed resolution, release it after completion, and preserve the existing
handling of None decisions so deduplications are not silently dropped.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6dd7ff91-5657-4029-8e4f-d29932ae8e75

📥 Commits

Reviewing files that changed from the base of the PR and between a5488d0 and 58c9a84.

📒 Files selected for processing (3)
  • rag/advanced_rag/knowlege_compile/_common.py
  • rag/advanced_rag/knowlege_compile/structure.py
  • rag/svr/task_executor_refactor/dataset_wiki_generator.py

Comment thread rag/advanced_rag/knowlege_compile/_common.py
Comment thread rag/advanced_rag/knowlege_compile/_common.py Outdated

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
rag/svr/task_executor_refactor/dataset_wiki_generator.py (2)

875-880: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Cancel background workers to prevent deadlocks and orphaned tasks.

If await asyncio.gather(*producers) or await map_queue.join() raises an exception (or is cancelled), the function exits and leaves orphaned workers running in the background, causing resource leaks.
Additionally, if any worker dies prematurely, await map_queue.put(None) can hang indefinitely because there aren't enough alive workers to consume the poison pills, deadlocking the cleanup phase.

Since await map_queue.join() guarantees all work is done and workers are idle, you can safely and robustly shut them down by cancelling them in a finally block, removing the need for poison pills entirely.

🛡️ Proposed robust cleanup
-    await asyncio.gather(*producers)
-    await map_queue.join()
-    for _ in workers:
-        await map_queue.put(None)
-    await asyncio.gather(*workers)
+    try:
+        await asyncio.gather(*producers)
+        await map_queue.join()
+    finally:
+        for w in workers:
+            if not w.done():
+                w.cancel()
+        # Await the cancelled workers to suppress "Task was destroyed but it is pending" warnings
+        await asyncio.gather(*workers, return_exceptions=True)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rag/svr/task_executor_refactor/dataset_wiki_generator.py` around lines 875 -
880, Update the producer/worker coordination around asyncio.gather and
map_queue.join to use a finally block that cancels every task in workers and
awaits their completion, including cancellation/error paths. Remove the
poison-pill map_queue.put(None) shutdown loop, while preserving normal producer
completion and queue draining behavior.

832-834: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Move item unpacking inside the try block to prevent silent worker crashes and deadlocks.

If an exception occurs during unpacking or dictionary access (e.g., if doc is missing an "id"), the worker will crash without executing the finally block. This prevents map_queue.task_done() from being called, which causes await map_queue.join() to hang indefinitely and deadlock the entire pipeline.

🛡️ Proposed fix to ensure queue completion
-            i, doc, template_id, parser_cfg, batch, batch_no = item
-            doc_id = doc["id"]
-            stats = doc_stats[i]
             try:
+                i, doc, template_id, parser_cfg, batch, batch_no = item
+                doc_id = doc["id"]
+                stats = doc_stats[i]
                 map_llm_id = (parser_cfg.get("llm_id") or "").strip() if isinstance(parser_cfg, dict) else ""
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rag/svr/task_executor_refactor/dataset_wiki_generator.py` around lines 832 -
834, Move the item unpacking and subsequent doc access in the worker flow,
including the assignments to i, doc, template_id, parser_cfg, batch, batch_no,
doc_id, and stats, inside the existing try block so exceptions still reach the
finally block and map_queue.task_done() is always called.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rag/advanced_rag/knowlege_compile/_common.py`:
- Around line 502-504: Preserve 1:1 batch-to-result cardinality in the batch
processing flow: in rag/advanced_rag/knowlege_compile/_common.py lines 502-504,
update the loop around process_batch to enqueue every entries value, including
empty batches; in lines 521-522, remove the early return for batches containing
no entries so process_batch produces the corresponding safe defaults. Both sites
require direct changes.

---

Outside diff comments:
In `@rag/svr/task_executor_refactor/dataset_wiki_generator.py`:
- Around line 875-880: Update the producer/worker coordination around
asyncio.gather and map_queue.join to use a finally block that cancels every task
in workers and awaits their completion, including cancellation/error paths.
Remove the poison-pill map_queue.put(None) shutdown loop, while preserving
normal producer completion and queue draining behavior.
- Around line 832-834: Move the item unpacking and subsequent doc access in the
worker flow, including the assignments to i, doc, template_id, parser_cfg,
batch, batch_no, doc_id, and stats, inside the existing try block so exceptions
still reach the finally block and map_queue.task_done() is always called.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2add3c4a-1aa8-4814-be25-f188e083223f

📥 Commits

Reviewing files that changed from the base of the PR and between 58c9a84 and 5481929.

📒 Files selected for processing (2)
  • rag/advanced_rag/knowlege_compile/_common.py
  • rag/svr/task_executor_refactor/dataset_wiki_generator.py

Comment thread rag/advanced_rag/knowlege_compile/_common.py Outdated

@wangq8 wangq8 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

PR Review: refa: optimize wiki compilation concurrency

Thanks for this PR. The overall direction — shared LLM pool, blockwise embedding dedup, and concurrent disambiguation / REFINE — is solid and should meaningfully improve Wiki compilation throughput. A few observations:


✅ What looks good

  1. run_chunked_pipeline — Producer-consumer pattern via asyncio.Queue is cleaner than the old Semaphore + gather approach. It avoids creating all tasks upfront and the sentinel-based worker termination is correct. Exception propagation is also handled properly (worker crash → gather raises → cancel + re-raise).

  2. Blockwise cosine similarity — Replacing sklearn.cosine_similarity (N×N materialization) with blockwise matrix @ block.T is a strong improvement for large KBs. The grouping by type_key and chunk-by-chunk triu_indices / np.nonzero logic correctly covers all pairs without duplication.

  3. LLMCallPool backpressure — Adding max_pending with the pending_count guard keeps queue growth bounded. Since the existing code uses notify_all() everywhere, no deadlock risk; the while pending_count >= max_pending re-check after wakeup is correct.

  4. Concurrent _resolve_ambiguous_pairs — Deferring union-find mutations to a single serial pass after asyncio.gather avoids race conditions on merged_into. The docstring clearly explains the intent.

  5. Template resolution upfront — Moving template resolution before the MAP worker pool avoids subtle ordering dependencies on which document happens to finish first.


⚠️ Items worth addressing

1. WIKI_MAP_QUEUE_SIZE = 5 with 20 workers may starve workers

With 20 map workers but a queue capacity of only 5 batches, workers will frequently block on map_queue.get(). Since each batch triggers expensive LLM calls, this may be acceptable in practice, but the ratio feels off. Consider at least matching WIKI_MAP_LLM_POOL_SIZE (20), or document why 5 was chosen (e.g. ES fetch latency is the bottleneck, not the queue).

2. _resolve_ambiguous_pairs sends all pairs to LLM without pre-filtering

The old code filtered _root(i) != _root(j) before each batch's LLM call, avoiding wasted LLM queries for already-merged transitive pairs. The new concurrent approach can't do that pre-filter (no ordering guarantee), which is correct — but it means every ambiguous pair always triggers an LLM call, even if transitive merges in concurrent batches would have made it redundant. This is a compute-cost trade-off vs. latency. Worth a comment in the code acknowledging it.

3. _embedding_dedup normalization change for zero-magnitude vectors

np.divide(matrix, norms, out=np.zeros_like(matrix), where=norms > 0) produces zero vectors for zero-magnitude embeddings, which then yield similarity 0. The old cosine_similarity would produce NaN. In practice zero embeddings are rare, so this is low risk, but the behavioral diff is worth noting.

4. Progress granularity reduced

Progress is now reported only when each document starts (inside _produce_document), not per-batch. For documents with many batches, the progress bar will stall at a single value for a long time. Consider passing per-batch progress info back from workers via a shared counter, or accept the coarser granularity (MAP is only ~60% of the total progress range anyway).

5. No test additions

This PR touches 3 files with significant concurrency / algorithmic changes and no test coverage. At minimum, adding a basic test for the LLMCallPool max_pending backpressure behavior and the blockwise dedup logic would increase confidence.


🔍 Minor / style

  • The _call_model inner function inside _resolve_batch just wraps asyncio.wait_for(gen_json(...), timeout=llm_timeout) — it could be inlined.
  • persist_wiki_pages_to_es / persist_wiki_page_graph_to_es switch to keyword args: cosmetic, parameter names match, so it's compatible.
  • _pool_managed = True on PooledChatModel appears unused in this PR — is it for a follow-up?

Verdict: Approve with suggestions. The core design is sound; the queue sizing and the ambiguity pre-filtering trade-off are the main things to discuss or document.

@codecov

codecov Bot commented Jul 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.65%. Comparing base (3670b04) to head (60861d1).
⚠️ Report is 38 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #17192      +/-   ##
==========================================
- Coverage   94.56%   90.65%   -3.91%     
==========================================
  Files          10       10              
  Lines         717      717              
  Branches      118      118              
==========================================
- Hits          678      650      -28     
- Misses         25       39      +14     
- Partials       14       28      +14     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
rag/svr/task_executor_refactor/dataset_wiki_generator.py (1)

767-777: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the unused template_id from MAP job tuples.

After resolving parser_cfg, template_id is never consumed; Ruff reports RUF059 at Line 832. Remove it from resolved_eligible, producer arguments, and queue items instead of carrying a dead field.

Proposed fix
-    resolved_eligible: list[tuple[dict, str, dict]] = []
+    resolved_eligible: list[tuple[dict, dict]] = []
...
-        resolved_eligible.append((doc, template_id, template.get("config") or {}))
+        resolved_eligible.append((doc, template.get("config") or {}))
...
-        for i, (doc, _, _) in enumerate(resolved_eligible)
+        for i, (doc, _) in enumerate(resolved_eligible)
...
-    async def _produce_document(i: int, job: tuple[dict, str, dict]) -> None:
-        doc, template_id, parser_cfg = job
+    async def _produce_document(i: int, job: tuple[dict, dict]) -> None:
+        doc, parser_cfg = job
...
-                await map_queue.put((i, doc, template_id, parser_cfg, batch, stats["batch_count"]))
+                await map_queue.put((i, doc, parser_cfg, batch, stats["batch_count"]))
...
-                i, doc, template_id, parser_cfg, batch, batch_no = item
+                i, doc, parser_cfg, batch, batch_no = item

Also applies to: 807-832

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rag/svr/task_executor_refactor/dataset_wiki_generator.py` around lines 767 -
777, Remove the unused template_id field from the resolved_eligible tuple and
all downstream MAP job data. Update the producer logic around
CompilationTemplateService.get_saved and the queue-item construction/consumption
near the parser_cfg handling so tuples and arguments contain only the values
that are actually used, including adjusting unpacking consistently.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@rag/svr/task_executor_refactor/dataset_wiki_generator.py`:
- Around line 767-777: Remove the unused template_id field from the
resolved_eligible tuple and all downstream MAP job data. Update the producer
logic around CompilationTemplateService.get_saved and the queue-item
construction/consumption near the parser_cfg handling so tuples and arguments
contain only the values that are actually used, including adjusting unpacking
consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 34828ca2-3b24-43e1-ac49-2615a7e81cfe

📥 Commits

Reviewing files that changed from the base of the PR and between 5481929 and 60861d1.

📒 Files selected for processing (3)
  • rag/advanced_rag/knowlege_compile/_common.py
  • rag/advanced_rag/knowlege_compile/structure.py
  • rag/svr/task_executor_refactor/dataset_wiki_generator.py
💤 Files with no reviewable changes (1)
  • rag/advanced_rag/knowlege_compile/structure.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • rag/advanced_rag/knowlege_compile/_common.py

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

Labels

ci Continue Integration 🌈 python Pull requests that update Python code ☯️ refactor Pull request that refactor/refine code size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants