Skip to content

perf: parallelize the pipeline + add a Groq rate limiter - #7

Merged
RudraDudhat2509 merged 1 commit into
mainfrom
perf/async-pipeline
Jun 19, 2026
Merged

perf: parallelize the pipeline + add a Groq rate limiter#7
RudraDudhat2509 merged 1 commit into
mainfrom
perf/async-pipeline

Conversation

@RudraDudhat2509

Copy link
Copy Markdown
Owner

Closes #6.

makes the pipeline actually fast. the runner was already concurrent; three other stages ran fully sequentially and ate all the wall-clock time.

whats parallel now

  • ontology.build_anchors - was ~16 LLM calls one at a time. now gathered, bounded by a semaphore. parse logic pulled into a _parse_anchors staticmethod.
  • generator.generate_test_cases - 4 buckets were sequential. now the 4 run concurrently via gather.
  • judge - cli.py looped await judge_single once per test (the biggest cost). new judge_all() gathers them with a semaphore; cli builds DiffResults from the ordered results.

behavior is identical - same results, same order, just concurrent.

rate limiter

the new concurrency would happily burst past Groq's free-tier RPM and eat 429s. semaphores bound concurrency, not rate, so they dont help here. added a token-bucket limiter (diffprompt/ratelimit.py) on every Groq request - that bounds rate, which is the thing that actually trips the limit.

  • default 30 RPM, safe for free tier
  • DIFFPROMPT_GROQ_RPM=0 disables it, higher values for paid tiers
  • local/Ollama calls are never throttled

tests

  • test_ratelimit.py - disabled no-op, initial burst, pacing after drain
  • test_judge.py - judge_all preserves order + count
  • test_ontology.py - build_anchors covers every tag (fake embedder, no model load) + _parse_anchors
  • generator bucket-coverage test

38 passed locally (was 30).

note

the queue-vs-gather question came up. for a fixed fan-out (known set of calls, collect results) gather+semaphore is simpler and equivalent. a queue/worker-pool earns its keep once we add streaming/continuous monitoring - not here.

…iter

three stages ran fully sequentially and dominated wall-clock time. the
runner was already concurrent; these were not:

- ontology.build_anchors: ~16 calls one at a time -> gather, bounded by a
  semaphore. pulled the parse logic into a _parse_anchors staticmethod.
- generator.generate_test_cases: 4 buckets one at a time -> gather the 4.
- judge: cli looped `await judge_single` per test -> new judge_all() gathers
  with a semaphore, cli builds DiffResults from the ordered results.

behavior is identical, just concurrent.

also added a token-bucket rate limiter (diffprompt/ratelimit.py) on every
groq request so the new concurrency doesnt burst past the free-tier RPM cap.
semaphores bound concurrency; this bounds rate, which is the thing that
actually trips 429s. tune with DIFFPROMPT_GROQ_RPM (default 30, 0 disables).

tests: test_ratelimit, test_judge, test_ontology + a generator bucket test.
38 passed locally.

closes #6

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@RudraDudhat2509
RudraDudhat2509 merged commit 0a67d62 into main Jun 19, 2026
3 checks passed
@RudraDudhat2509
RudraDudhat2509 deleted the perf/async-pipeline branch June 19, 2026 11:22
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.

Pipeline is slow: parallelize anchors, generation, and the judge loop

1 participant