mirror of
https://github.com/safishamsi/graphify.git
synced 2026-09-24 14:35:46 +00:00
feat: parallel community labeling via --max-concurrency / --batch-size (#1390)
label_communities ran batches one LLM call at a time, so a large graph needed hundreds of sequential calls even on backends that allow heavy concurrency. It now fans batches out across a thread pool, mirroring extract_corpus_parallel: results are returned per batch and merged on the main thread (labels dict is never mutated concurrently, no lock), and workers==1 keeps the original sequential path verbatim. ollama and claude-cli are forced serial unless the matching GRAPHIFY_*_PARALLEL env opt-in is set (same guard as extract). generate_community_labels threads max_concurrency + batch_size through, and the cluster-only/label CLI parses --max-concurrency and --batch-size (both `--flag N` and `--flag=N` forms; the space form is parsed explicitly so the value is not mistaken for the positional scan path by the arg-walk's catch-all). Output is deterministic regardless of concurrency (keyed by community id). Tests: parallel == sequential result, batch-size controls batch count, batches actually run concurrently, ollama forced serial, and the CLI parses both new flags. Full suite 2393 passed; ruff clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
1a14e94e53
commit
22a58ffc20
+92
-2
@@ -81,9 +81,12 @@ def test_label_cli_passes_model_override(tmp_path, monkeypatch):
|
||||
|
||||
captured = {}
|
||||
|
||||
def fake_generate(G, communities, *, backend=None, model=None, gods=None, quiet=False):
|
||||
def fake_generate(G, communities, *, backend=None, model=None, gods=None,
|
||||
quiet=False, max_concurrency=4, batch_size=100):
|
||||
captured["backend"] = backend
|
||||
captured["model"] = model
|
||||
captured["max_concurrency"] = max_concurrency
|
||||
captured["batch_size"] = batch_size
|
||||
return {0: "Orders"}, "llm"
|
||||
|
||||
monkeypatch.setattr("graphify.llm.generate_community_labels", fake_generate)
|
||||
@@ -99,13 +102,22 @@ def test_label_cli_passes_model_override(tmp_path, monkeypatch):
|
||||
"gemini",
|
||||
"--model",
|
||||
"gemini-3.1-flash-lite",
|
||||
"--max-concurrency",
|
||||
"8",
|
||||
"--batch-size",
|
||||
"50",
|
||||
"--no-viz",
|
||||
],
|
||||
)
|
||||
|
||||
cli.main()
|
||||
|
||||
assert captured == {"backend": "gemini", "model": "gemini-3.1-flash-lite"}
|
||||
# Also verifies the space-separated forms parse (the value must not be mistaken
|
||||
# for the positional path) and reach generate_community_labels.
|
||||
assert captured == {
|
||||
"backend": "gemini", "model": "gemini-3.1-flash-lite",
|
||||
"max_concurrency": 8, "batch_size": 50,
|
||||
}
|
||||
|
||||
|
||||
def test_label_communities_partial_reply_fills_placeholder(monkeypatch):
|
||||
@@ -278,3 +290,81 @@ def test_label_communities_max_communities_caps_total(monkeypatch):
|
||||
label_communities(G, communities, backend="gemini", max_communities=40, batch_size=100)
|
||||
# Only 40 communities should have been sent to the backend.
|
||||
assert len(captured_cids) == 40
|
||||
|
||||
|
||||
# --- #1390: parallel labeling (--max-concurrency) + --batch-size --------------
|
||||
|
||||
import threading
|
||||
import time as _time
|
||||
|
||||
|
||||
def _many_communities(n):
|
||||
G = nx.Graph()
|
||||
comms = {}
|
||||
for i in range(n):
|
||||
nid = f"n{i}"
|
||||
G.add_node(nid, label=f"sym_{i}")
|
||||
comms[i] = [nid]
|
||||
return G, comms
|
||||
|
||||
|
||||
def test_label_communities_parallel_matches_sequential(monkeypatch):
|
||||
"""Concurrency must not change the result: same cid->name map either way."""
|
||||
G, communities = _many_communities(6)
|
||||
|
||||
def fake_batch(batch_cids, batch_lines, *, backend, model=None):
|
||||
return {cid: f"name-{cid}" for cid in batch_cids}
|
||||
|
||||
monkeypatch.setattr("graphify.llm._label_batch_with_retry", fake_batch)
|
||||
seq = label_communities(G, communities, backend="gemini", batch_size=1, max_concurrency=1)
|
||||
par = label_communities(G, communities, backend="gemini", batch_size=1, max_concurrency=4)
|
||||
assert seq == par == {i: f"name-{i}" for i in range(6)}
|
||||
|
||||
|
||||
def test_label_communities_batch_size_controls_batch_count(monkeypatch):
|
||||
G, communities = _many_communities(5)
|
||||
calls = []
|
||||
|
||||
def fake_batch(batch_cids, batch_lines, *, backend, model=None):
|
||||
calls.append(list(batch_cids))
|
||||
return {cid: f"n-{cid}" for cid in batch_cids}
|
||||
|
||||
monkeypatch.setattr("graphify.llm._label_batch_with_retry", fake_batch)
|
||||
labels = label_communities(G, communities, backend="gemini", batch_size=2, max_concurrency=1)
|
||||
assert len(calls) == 3 # 5 communities / batch 2 -> 3 batches
|
||||
assert sum(len(c) for c in calls) == 5
|
||||
assert labels == {i: f"n-{i}" for i in range(5)}
|
||||
|
||||
|
||||
def _peak_tracker():
|
||||
lock = threading.Lock()
|
||||
state = {"now": 0, "peak": 0}
|
||||
|
||||
def fake_batch(batch_cids, batch_lines, *, backend, model=None):
|
||||
with lock:
|
||||
state["now"] += 1
|
||||
state["peak"] = max(state["peak"], state["now"])
|
||||
_time.sleep(0.03)
|
||||
with lock:
|
||||
state["now"] -= 1
|
||||
return {cid: f"n-{cid}" for cid in batch_cids}
|
||||
|
||||
return fake_batch, state
|
||||
|
||||
|
||||
def test_label_communities_runs_batches_concurrently(monkeypatch):
|
||||
G, communities = _many_communities(8)
|
||||
fake_batch, state = _peak_tracker()
|
||||
monkeypatch.setattr("graphify.llm._label_batch_with_retry", fake_batch)
|
||||
label_communities(G, communities, backend="gemini", batch_size=1, max_concurrency=4)
|
||||
assert state["peak"] > 1, "batches should run in parallel with max_concurrency>1"
|
||||
|
||||
|
||||
def test_label_communities_forces_serial_for_ollama(monkeypatch):
|
||||
"""ollama/claude-cli must stay serial regardless of --max-concurrency."""
|
||||
G, communities = _many_communities(8)
|
||||
fake_batch, state = _peak_tracker()
|
||||
monkeypatch.setattr("graphify.llm._label_batch_with_retry", fake_batch)
|
||||
monkeypatch.delenv("GRAPHIFY_OLLAMA_PARALLEL", raising=False)
|
||||
label_communities(G, communities, backend="ollama", batch_size=1, max_concurrency=8)
|
||||
assert state["peak"] == 1, "ollama must be forced serial"
|
||||
|
||||
Reference in New Issue
Block a user