{{ message }}
Use async Cohere client in CohereReranker - #1522
Open
edwinyyyu wants to merge 1 commit into
Open
Conversation
Running the sync ClientV2 rerank call through asyncio.to_thread caps concurrent reranks at the default executor size, min(32, cpu_count + 4) threads, each pinned for the full network round trip. Switching to AsyncClientV2 makes rerank loop-native I/O with no thread cap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Edwin Yu <edwinyyyu@gmail.com>
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.

Purpose of the change
Remove a hard throughput ceiling on Cohere reranking.
CohereReranker.scorecurrently runs the syncClientV2.rerankcall throughasyncio.to_thread, which draws from asyncio's default executor: at mostmin(32, cpu_count + 4)worker threads. Each in-flight rerank pins one of those threads for its full network round trip (typically ~0.3-0.5 s), so rerank throughput is capped at roughlythreads / latencyRPS regardless of how much concurrency the caller offers:Beyond the cap, calls queue inside the executor and observed latency grows linearly with load. The default executor is also shared with every other
asyncio.to_threadcall in the process (e.g.SentenceTransformerEmbedder,CrossEncoderReranker), so saturating it with reranks starves local model inference too.This PR switches to the Cohere SDK's native async client (
AsyncClientV2, httpx-based), making rerank plain event-loop I/O. Concurrency is then bounded by the HTTP connection pool and the API itself, not by a thread pool.Description
cohere_reranker.py:scorenow awaitsclient.rerank(...)directly; theasyncio.to_threadwrapper is gone. Score mapping and empty/blank input handling are unchanged.reranker_manager.py:_build_cohere_rerankerconstructsAsyncClientV2(sameapi_key/base_urlkwargs asClientV2).cohere_clientfixture and the reranker manager tests updated toAsyncClientV2; new keyless unit tests added (see below).No config or API surface changes;
AsyncClientV2is generated from the same spec asClientV2and is already part of the pinnedcohere>=5.20.0dependency.Measured effect (stub client with 0.2 s simulated latency): 128 concurrent
score()calls complete in 0.20 s wall on this branch, vs ~1.7 s when serialized through a 15-thread default executor. The gap widens with load.Breaking for usage as a library.
Type of change
How Has This Been Tested?
Unit tests (no API key needed): new
test_cohere_reranker_unit.pyrunsCohereRerankeragainst a stub async client: relevance scores map back to original candidate positions, empty/blank candidates skip the API, blank queries are replaced, request parameters pass through verbatim, client errors are wrapped inExternalServiceAPIError, and a 64-way concurrency canary fails by timeout if calls ever get serialized through a bounded worker pool. Full reranker + reranker manager suite: 186 passed, ruff clean.Wire equivalence (no API key needed): pointed both
ClientV2(current production path) andAsyncClientV2at a local capture HTTP server viabase_urland issued the same rerank call. Results: byte-identical requests (POST /v2/rerank, identical JSON body, no header differences includingauthorization), both parse into equalV2RerankResponseobjects, andCohereRerankerend-to-end over real HTTP returns correctly mapped scores. Since the async client emits exactly the bytes the sync client already sends in production, server-side behavior is unchanged by this PR.Integration: the existing
test_cohere_reranker.py(integration-marked) is updated to the async client with identical coverage; it needsCOHERE_API_KEYto run. Note the pytest-integration workflow does not currently set that secret, so these tests also skip in CI today - the keyless wire-equivalence check above was run in their place.Test Results:
Checklist
Screenshots/Gifs
N/A
Further comments
httpx_clienttoAsyncClientV2; left at defaults here.AmazonBedrockRerankerhas the same to_thread ceiling (boto3 has no async client); out of scope for this PR.🤖 Generated with Claude Code