fix(examples): forward full ensemble requests and cap concurrent explorations - #202
Conversation
08758e1 to
72d0323
Compare
72d0323 to
b545e05
Compare
WalkthroughThe ensemble router now uses reservation-based exploration state with asynchronous completion notifications. It preserves structured requests through candidate and committed paths, aggregates streamed candidate responses for judging, and adds concurrency and failure-handling tests. ChangesEnsemble router
Estimated code review effort: 4 (Complex) | ~45 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
crates/libsy/examples/ensemble.rs (1)
313-335: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winCandidate streams are drained serially — fold aggregation into the fan-out futures.
join_allonly awaits the call handshake; withstream = truenow forwarded to candidates, the actual token streams are consumed one after another here, so candidate n's stream isn't polled until n-1 is fully drained. Aggregating inside each candidate future keeps the whole fan-out concurrent.♻️ Aggregate each candidate inside its own future
calls.push(async move { - ( - model, - driver - .call_llm_target(ctx, &target, call_request, decision) - .await, - ) + let aggregated = match driver + .call_llm_target(ctx, &target, call_request, decision) + .await + { + Ok(Response { + llm_response, + metadata, + }) => llm_response.into_agg().await.map(|agg| Response { + llm_response: LlmResponse::Agg(agg), + metadata, + }), + Err(error) => Err(error.into()), + }; + (model, aggregated) }); } let results = futures::future::join_all(calls).await; - // Aggregate each successful candidate before judging: a streamed candidate - // must be read to completion so the judge can compare its text, and the - // winner is returned as this buffered aggregate. A candidate whose call or - // stream fails is excluded rather than failing the turn. + // A candidate whose call or stream failed is excluded rather than failing + // the turn; the winner is returned as its buffered aggregate. let mut survivors: Vec<(String, Response)> = Vec::new(); for (model, result) in results { - let Ok(response) = result else { continue }; - let Response { - llm_response, - metadata, - } = response; - if let Ok(agg) = llm_response.into_agg().await { - survivors.push(( - model, - Response { - llm_response: LlmResponse::Agg(agg), - metadata, - }, - )); - } + let Ok(response) = result else { continue }; + survivors.push((model, response)); }Note the error types on both arms need to line up (
Result/LibsyError) — adjust the conversion to whatevercall_llm_targetandinto_aggreturn.🤖 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 `@crates/libsy/examples/ensemble.rs` around lines 313 - 335, Move LlmResponse::into_agg aggregation into each candidate future before join_all in the ensemble fan-out, so stream consumption remains concurrent. Update the call_llm_target future’s success and failure branches to return a consistent Result/LibsyError type, then collect only successful aggregated Responses into survivors while preserving model and metadata.
🤖 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.
Nitpick comments:
In `@crates/libsy/examples/ensemble.rs`:
- Around line 313-335: Move LlmResponse::into_agg aggregation into each
candidate future before join_all in the ensemble fan-out, so stream consumption
remains concurrent. Update the call_llm_target future’s success and failure
branches to return a consistent Result/LibsyError type, then collect only
successful aggregated Responses into survivors while preserving model and
metadata.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 64595890-9090-4864-b996-8a6b28b27915
📒 Files selected for processing (1)
crates/libsy/examples/ensemble.rs
ad35865 to
328c5bc
Compare
|
@coderabbitai review |
✅ Action performedReview finished.
|
…orations Signed-off-by: Elyas Mehtabuddin <emehtabuddin@nvidia.com>
328c5bc to
543f249
Compare
|
What happens
The ensemble example rebuilds each candidate request from the last user message. The candidates and the model chosen after exploration receive this smaller request:
The system message, tools, tool choice, sampling settings, and stream flag are missing, so the candidates answer a different request.
The example also counts an exploration only after the candidate calls finish. With
exploration_turns = 1, two requests that arrive together can both explore:Fix
The example now sends the caller's complete request to every candidate and to the chosen model. It reads candidate streams concurrently before asking the judge to choose, so a slow stream does not delay reading the others.
A request claims an exploration slot before its first model call. Requests wait when all slots are claimed, and a dropped or failed turn returns its slot before a winner is recorded. The completed win counts determine when the example can choose one model.
The example still buffers candidate streams because it must read every candidate response before it can choose a winner.
Evidence
With
exploration_turns = 1, two simultaneous requests now make these calls whenb/modelwins the first request:The regression tests also show that:
This is a correctness fix in an example, not a performance change, so there is no benchmark.
Testing