Skip to content

feat(kg-extract): concurrent backfill via streaming task pool - #217

Merged
jphein merged 1 commit into
mainfrom
feat/kg-extract-concurrency
May 26, 2026
Merged

feat(kg-extract): concurrent backfill via streaming task pool#217
jphein merged 1 commit into
mainfrom
feat/kg-extract-concurrency

Conversation

@jphein

@jphein jphein commented May 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

Five tightly-coupled changes that together unstick KG backfill throughput
after PR #208 removed the asyncio.Lock bottleneck.

  1. Semaphore narrowed to the LLM call only. Extracted _extract_under_sem so the slot is held during extract_triples and released before AGE writes begin. Previously slow DB writes held LLM slots, starving llama-server's --parallel capacity.
  2. Streaming task pool replaces the gather-over-batch barrier. Old loop: claim → asyncio.gather(*batch) → next claim — slowest drawer stalled the next claim cycle. New model: one producer task tops up an asyncio.Queue whenever it dips below batch_size // 2; max_concurrency persistent consumer tasks pull and run _process_one. No batch boundaries.
  3. Default knobs bumped: DEFAULT_CONCURRENCY 8 → 24 (matches llama-server --parallel), _SyncConnPool.max_size 10 → 32 (covers 24 LLM slots + slack).
  4. New --db-pool-size CLI flag (env: MEMPALACE_KG_DB_POOL_SIZE). Validated db_pool_size >= max_concurrency so every in-flight LLM call is guaranteed a write conn. Default is max_concurrency + 8.
  5. DEFAULT_ENDPOINT switched from http://familiar.jphe.in:11436 to http://familiar:11436 — bare Tailscale hostname for inter-host calls.

Why these belong in one PR

Changes #1#4 are interlocking: narrowing the sem only helps if the producer/consumer model doesn't serialize on batch boundaries; bumping concurrency without bumping the DB pool would starve writes; the --db-pool-size flag exists precisely so #3 can be retuned at deploy time. #5 is a one-line stale-URL fix bundled here because it would otherwise need its own PR for a single literal change.

Test plan

  • ruff check mempalace/kg_triple_worker.py tests/test_kg_triple_worker.py — clean
  • ruff format --check — clean
  • pytest tests/test_kg_triple_worker.py — 23/23 passing, including new coverage for:
    • Streaming-pool processes more than one batch without a gather barrier
    • Consumer task count equals max_concurrency
    • Sem releases its LLM slot before the first DB write
    • db_pool_size < max_concurrency raises ValueError
    • --db-pool-size flag threads through to pool_factory
    • Default db_pool_size is max_concurrency + 8
    • Default endpoint uses bare Tailscale host
    • Default concurrency = 24 (matches llama-server)
    • Default pool max_size = 32
  • CI: full test-linux matrix on the runners

Related

Five tightly-coupled changes that together unstick KG backfill throughput
after PR #208 removed the asyncio.Lock bottleneck:

1. Semaphore narrowed to the LLM call only.
   Extracted ``_extract_under_sem`` so the ``asyncio.Semaphore`` slot is
   held during ``extract_triples`` and released before any AGE writes
   begin. Previously a slow DB write held an LLM slot, starving llama
   server's ``--parallel`` capacity.

2. Streaming task pool replaces the gather-over-batch barrier.
   The old loop did claim → ``asyncio.gather(*batch)`` → next claim,
   so the slowest drawer in each batch stalled the next claim. New
   model: one producer task tops up an ``asyncio.Queue`` whenever it
   dips below ``batch_size // 2``; ``max_concurrency`` persistent
   consumer tasks pull and run ``_process_one``. No batch boundaries.

3. Default knobs bumped for the new floor:
   - ``DEFAULT_CONCURRENCY`` 8 → 24 (matches llama-server --parallel)
   - ``_SyncConnPool.max_size`` 10 → 32 (covers 24 LLM slots + slack)

4. New ``--db-pool-size`` CLI flag (env: ``MEMPALACE_KG_DB_POOL_SIZE``).
   Validated ``db_pool_size >= max_concurrency`` so every in-flight LLM
   call is guaranteed a write conn. Default is ``max_concurrency + 8``.

5. ``DEFAULT_ENDPOINT`` switched from ``http://familiar.jphe.in:11436``
   to ``http://familiar:11436``. The FQDN was stale; bare ``familiar``
   resolves via Tailscale for inter-host calls.

Tests: 23/23 in tests/test_kg_triple_worker.py — including new coverage
for streaming-pool no-barrier behaviour, consumer count = max_concurrency,
sem-release-before-DB-write, db-pool-size validation, and the bare-host
endpoint default.
Copilot AI review requested due to automatic review settings May 26, 2026 11:28
@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request optimizes the KG backfill throughput by decoupling LLM inference from database write operations. By moving to a streaming producer-consumer model and narrowing the semaphore scope, the worker can maintain higher concurrency without being bottlenecked by individual slow database transactions. These changes are supported by updated default resource limits and a new database pool configuration flag.

Highlights

  • Semaphore Narrowing: Extracted the LLM extraction logic into _extract_under_sem to ensure the semaphore is released before database writes begin, preventing DB-bound tasks from starving LLM concurrency slots.
  • Streaming Task Pool: Replaced the batch-barrier asyncio.gather model with a producer-consumer queue architecture, allowing continuous processing and preventing slow drawers from stalling the entire pipeline.
  • Configuration & Tuning: Increased DEFAULT_CONCURRENCY to 24 and max_size to 32 to better align with llama-server capacity, and introduced a new --db-pool-size CLI flag for fine-grained resource management.
  • Infrastructure Fix: Updated DEFAULT_ENDPOINT to use a bare Tailscale hostname for improved inter-host connectivity.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the worker concurrency model in kg_triple_worker.py to use a producer/consumer pattern with an asyncio.Queue. This decoupling prevents slow database writes from stalling the LLM processing pipeline. Additionally, the semaphore scope is narrowed to only wrap the LLM extraction step. The review feedback suggests a highly efficient consumer shutdown mechanism by pushing None sentinels to the queue upon producer completion, avoiding busy-waiting.

Comment on lines +725 to +726
finally:
producer_done.set()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

To support a non-polling, highly efficient consumer shutdown, update the producer's finally block to push a None sentinel for each consumer task. This avoids busy-waiting in the consumers when the queue is empty.

Suggested change
finally:
producer_done.set()
finally:
producer_done.set()
for _ in range(max_concurrency):
await work_queue.put(None)

@jphein
jphein merged commit c366e4a into main May 26, 2026
@jphein
jphein deleted the feat/kg-extract-concurrency branch May 26, 2026 11:39
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.

1 participant