Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
2398055
refactor(runtime/network): consolidate RequestControlMessage and add …
GuanLuo May 18, 2026
416cd35
feat(runtime/egress): add AddressedPushRouter::generate_bidirectional
GuanLuo May 18, 2026
ee5d274
feat(runtime/router): wire bidirectional PushRouter through Addressed…
GuanLuo May 18, 2026
0433966
feat(runtime/ingress): add BidirectionalIngress + PushWorkHandler imp…
GuanLuo May 18, 2026
8a93d07
refactor(runtime/router): reserve bidirectional worker up front befor…
GuanLuo May 18, 2026
321f1eb
refactor(runtime/transport): use header-only envelope for bidirection…
GuanLuo May 18, 2026
5c9f6df
refactor(runtime/pipeline): drop BidirectionalIngress; relax Frontend…
GuanLuo May 18, 2026
7bfa9e0
test(runtime/router): end-to-end bidirectional echo over the full Rus…
GuanLuo May 18, 2026
679175a
docs(runtime/router): replace speculative #[ignore] reason with empir…
GuanLuo May 18, 2026
0f198b6
fix(runtime/bidirectional): cancellation + serialize-error gaps from …
GuanLuo May 18, 2026
9817a75
refactor(runtime/ingress): extract pump_response_stream from PushWork…
GuanLuo May 18, 2026
a87a333
test(runtime): move bidirectional E2E test to integration binary; dro…
GuanLuo May 18, 2026
1dca505
fix(runtime/bidirectional): adapt to new 2/N API after rebase
GuanLuo May 21, 2026
2eee690
refactor(runtime/router): extract resolve_transport / check_workers /…
GuanLuo May 21, 2026
61ea902
refactor(runtime/egress): extract finalize_response_stream from Addre…
GuanLuo May 21, 2026
310fe83
refactor(runtime/egress): extract AddressedPushRouter::register_streams
GuanLuo May 22, 2026
9e47736
fix(runtime/tcp): track both halves in associate_instance / cancel_in…
GuanLuo May 22, 2026
7f6493b
feat(runtime/pipeline): promote ManyIn<T> to a struct carrying Contex…
GuanLuo May 25, 2026
2dccc6d
refactor(runtime/pipeline): rename ManyIn struct to RequestStream
GuanLuo May 26, 2026
e109fa7
refactor(runtime/pipeline): redesign ManyIn as Context<RequestStream<T>>
GuanLuo May 26, 2026
dc138b1
docs(runtime/pipeline): drop stale ManyIn references and historical p…
GuanLuo May 26, 2026
95a3e69
refactor(runtime/egress): parametrize bidirectional dispatch on enabl…
GuanLuo May 26, 2026
fac1c09
refactor(runtime/egress): extract AddressedPushRouter::resolve_subjects
GuanLuo May 26, 2026
29dd214
refactor(runtime/egress): extract AddressedPushRouter::dispatch_reque…
GuanLuo May 26, 2026
f679aac
refactor(runtime/egress): centralize cancellation cleanup behind Canc…
GuanLuo May 26, 2026
5a8496b
refactor(runtime/egress): fold RequestControlMessage construction int…
GuanLuo May 26, 2026
faf0852
refactor(runtime/egress): extract AddressedPushRouter::dispatch_buffer
GuanLuo May 26, 2026
5bb52f9
refactor(runtime/egress): split build_request_envelope from wire disp…
GuanLuo May 26, 2026
9af8715
perf(runtime/egress): start bidirectional request forwarder before aw…
GuanLuo May 26, 2026
5c7802f
refactor(runtime/egress): extract spawn_request_stream_forwarder
GuanLuo May 26, 2026
2635869
refactor(runtime/egress): resolve_subjects returns CancelGuard, drop …
GuanLuo May 26, 2026
787a803
refactor(runtime/egress): unify bidi+unary dispatch behind dispatch_a…
GuanLuo May 26, 2026
6d3f28a
docs(runtime/egress): trim WHAT-narration, caller refs, and stale lines
GuanLuo May 26, 2026
807efff
docs(runtime/egress): restore pre-existing comments not added by this…
GuanLuo May 26, 2026
93816e3
refactor(runtime/pipeline): restore Frontend/SegmentSource/Ingress Sy…
GuanLuo May 27, 2026
0354ba6
refactor(runtime/ingress): move pump_response_stream into push_handle…
GuanLuo May 28, 2026
87e6c13
refactor(runtime/ingress): extract IngressDispatch strategy + handle_…
GuanLuo May 28, 2026
4ba0e89
refactor(runtime/egress): replace CancelGuard with RegisteredStream RAII
GuanLuo May 29, 2026
76bc89f
chore: update comments
GuanLuo May 29, 2026
1819f89
refactor(runtime/egress): derive engine_ctx in dispatch_and_finalize,…
GuanLuo May 29, 2026
fcb1b4e
refactor(runtime/egress): rename finalize_response_stream to decode_r…
GuanLuo May 29, 2026
3083ddf
docs(runtime/egress): move forwarder lifecycle detail from doc to bod…
GuanLuo May 29, 2026
5fa4735
chore: add comments
GuanLuo May 29, 2026
8f0cd99
refactor(runtime/pipeline): restore SegmentSink/ServiceBackend Sink S…
GuanLuo May 29, 2026
30f17f5
refactor(runtime/network): drop unused PartialEq on RequestType/Respo…
GuanLuo May 29, 2026
1ba3b64
feat(runtime/ingress): add bidirectional PushWorkHandler via IngressD…
GuanLuo May 29, 2026
2805e9b
docs(runtime/engine): stop AsyncEngineStream doc referencing pipeline…
GuanLuo May 29, 2026
c00e576
fix(runtime): return errors instead of panicking on bidirectional dis…
GuanLuo Jun 5, 2026
27fb870
feat(runtime/router): reject unsupported router modes for bidirection…
GuanLuo Jun 5, 2026
a5accd6
fix(runtime/ingress): bidirectional request forwarder honors stop_gen…
GuanLuo Jun 5, 2026
4caa158
style(runtime): address review nits on bidirectional dispatch
GuanLuo Jun 5, 2026
d66857e
refactor(runtime/ingress): share decode_control_message between unary…
GuanLuo Jun 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions lib/llm/src/engines.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@ use async_stream::stream;
use async_trait::async_trait;

use dynamo_runtime::engine::{AsyncEngine, AsyncEngineContextProvider, ResponseStream};
#[cfg(test)]
use dynamo_runtime::pipeline::RequestStream;
use dynamo_runtime::pipeline::{Error, ManyIn, ManyOut, SingleIn};
use dynamo_runtime::protocols::annotated::Annotated;
use futures::StreamExt;

#[cfg(test)]
use dynamo_runtime::engine::RequestStream;

use crate::protocols::openai::{
chat_completions::{NvCreateChatCompletionRequest, NvCreateChatCompletionStreamResponse},
completions::{NvCreateCompletionRequest, NvCreateCompletionResponse, prompt_to_string},
Expand Down Expand Up @@ -152,11 +151,15 @@ impl AsyncEngine<ManyIn<RealtimeClientEvent>, ManyOut<Annotated<RealtimeServerEv
{
async fn generate(
&self,
mut incoming: ManyIn<RealtimeClientEvent>,
incoming: ManyIn<RealtimeClientEvent>,
) -> Result<ManyOut<Annotated<RealtimeServerEvent>>, Error> {
let ctx = incoming.context();
let session_id = ctx.id().to_string();
let ctx_for_stream = ctx.clone();
let (request_stream, _ctx_unit) = incoming.into_parts();
let mut incoming = request_stream.take().ok_or_else(|| {
anyhow::anyhow!("RequestStream::take called twice on bidirectional input")
})?;

let output = stream! {
let ctx = ctx_for_stream;
Expand Down Expand Up @@ -561,7 +564,7 @@ mod tests {
use futures::stream;

fn make_input(events: Vec<RealtimeClientEvent>) -> ManyIn<RealtimeClientEvent> {
RequestStream::new(Box::pin(stream::iter(events)), Context::new(()).context())
Context::new(RequestStream::new(Box::pin(stream::iter(events))))
}

fn parse_client_event(json: serde_json::Value) -> RealtimeClientEvent {
Expand Down
6 changes: 3 additions & 3 deletions lib/llm/src/http/service/realtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ use axum::{
response::Response,
routing::get,
};
use dynamo_runtime::engine::{AsyncEngineContextProvider, RequestStream};
use dynamo_runtime::pipeline::Context;
use dynamo_runtime::engine::AsyncEngineContextProvider;
use dynamo_runtime::pipeline::{Context, RequestStream};
use futures::{SinkExt, StreamExt, stream::SplitSink};
use tokio::sync::mpsc;
use tokio_stream::wrappers::ReceiverStream;
Expand Down Expand Up @@ -145,7 +145,7 @@ async fn handle_socket(socket: WebSocket, state: Arc<service_v2::State>) {
}

let request_stream = Box::pin(ReceiverStream::new(req_rx));
let input = RequestStream::new(request_stream, Context::new(()).context());
let input = Context::new(RequestStream::new(request_stream));

let mut response_stream = match engine.generate(input).await {
Ok(s) => s,
Expand Down
21 changes: 3 additions & 18 deletions lib/runtime/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,14 +180,9 @@ pub trait AsyncEngineUnary<Resp: Data>:
///
/// This trait combines `Stream` semantics with context provider capabilities,
/// representing a continuous async operation that produces multiple messages over time.
///
/// - **Output side:** wrapped as [`EngineStream<T>`] = `crate::pipeline::ManyOut<T>`
/// — the stream of response chunks an engine emits.
/// - **Input side:** same `EngineStream<T>` shape, exposed as
/// `crate::pipeline::ManyIn<T>` for documentary clarity at the call site.
///
/// [`ResponseStream`] is the canonical concrete implementor; [`RequestStream`]
/// is a type alias of it for the input side.
/// The canonical boxed form is [`EngineStream<T>`] (= `crate::pipeline::ManyOut<T>`),
/// the stream of response chunks an engine emits; [`ResponseStream`] is the
/// canonical concrete implementor.
pub trait AsyncEngineStream<T: Data>: Stream<Item = T> + AsyncEngineContextProvider + Send {}

/// Engine is a trait that defines the interface for a streaming engine.
Expand Down Expand Up @@ -267,16 +262,6 @@ impl<R: Data> Debug for ResponseStream<R> {
}
}

/// Input-side type alias of [`ResponseStream`] — same struct, different name to
/// signal role at the call site.
///
/// The shape is identical: a `(stream, ctx)` pair that implements [`Stream`],
/// [`AsyncEngineContextProvider`], and [`AsyncEngineStream`]. Use `RequestStream`
/// when you're constructing a value to feed into the `Req` slot of an engine,
/// and [`ResponseStream`] when constructing a value to emit from the `Resp` slot.
/// Functionally interchangeable.
pub type RequestStream<R> = ResponseStream<R>;

impl<T: Data> AsyncEngineContextProvider for Pin<Box<dyn AsyncEngineUnary<T>>> {
fn context(&self) -> Arc<dyn AsyncEngineContext> {
AsyncEngineContextProvider::context(&**self)
Expand Down
54 changes: 40 additions & 14 deletions lib/runtime/src/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ pub use network::egress::push_router::{PushRouter, RouterMode, WorkerLoadMonitor
pub mod registry;

pub use crate::engine::{
self as engine, AsyncEngine, AsyncEngineContext, AsyncEngineContextProvider, Data, DataStream,
Engine, EngineStream, EngineUnary, RequestStream, ResponseStream, async_trait,
self as engine, AsyncEngine, AsyncEngineContext, AsyncEngineContextProvider, AsyncEngineStream,
Data, DataStream, Engine, EngineStream, EngineUnary, ResponseStream, async_trait,
};
pub use anyhow::Error;
pub use context::Context;
Expand All @@ -30,18 +30,44 @@ pub use error::{PipelineError, PipelineErrorExt, TwoPartCodecError};
/// about the request. This information propagates through the stages, both local and distributed.
pub type SingleIn<T> = Context<T>;

/// Pipeline input for streaming-request engines: a trait-object alias around a
/// stream that carries its own cancellation context (via the
/// [`AsyncEngineContextProvider`] half of [`AsyncEngineStream`]).
///
/// This is the input-side mirror of [`ManyOut`]. Both aliases resolve to the
/// same underlying [`EngineStream<T>`] type; the directional names are
/// documentary. Earlier definitions wrapped the stream in a `Context<…>`
/// (`Context<DataStream<T>>`); that shape was uninstantiable because
/// `DataStream<T>` is `!Sync` while `Context<T: Data>` requires `Sync`. The
/// trait-object form solves that cleanly: the cancellation surface is part of
/// the trait contract rather than an outer wrapper.
pub type ManyIn<T> = EngineStream<T>;
/// `Sync` ownership cell for a non-`Sync` [`DataStream<T>`]. [`Self::take`]
/// moves the inner stream out; the mutex serialises concurrent attempts so
/// the first caller observes `Some(stream)` and all later callers see
/// `None`. Iteration happens on the returned `DataStream<T>`.
pub struct RequestStream<T: Data> {
inner: std::sync::Mutex<Option<DataStream<T>>>,
}

impl<T: Data> RequestStream<T> {
/// Wrap a [`DataStream<T>`] in a `Sync` ownership cell.
pub fn new(stream: DataStream<T>) -> Self {
Self {
inner: std::sync::Mutex::new(Some(stream)),
}
}

/// Atomically move the inner stream out. Returns `Some(stream)` exactly
/// once across all threads racing on the same `RequestStream`; every
/// subsequent call (on any thread) returns `None`. The returned stream
/// is the unique owner; the wrapper retains nothing.
pub fn take(&self) -> Option<DataStream<T>> {
self.inner.lock().unwrap().take()
}
}

impl<T: Data> std::fmt::Debug for RequestStream<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let taken = self.inner.lock().map(|g| g.is_none()).unwrap_or(true);
f.debug_struct("RequestStream")
.field("taken", &taken)
.finish()
}
}

/// Pipeline input for streaming-request engines: a [`RequestStream<T>`]
/// payload wrapped in a [`Context`], symmetric to [`SingleIn<T>`] for unary
/// inputs.
pub type ManyIn<T> = Context<RequestStream<T>>;

/// Type alias for the output of pipeline that returns a single value
pub type SingleOut<T> = EngineUnary<T>;
Expand Down
11 changes: 9 additions & 2 deletions lib/runtime/src/pipeline/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@ use super::{AsyncEngine, AsyncEngineContext, AsyncEngineContextProvider, Respons
use serde::{Deserialize, Serialize};

use super::{
AsyncTransportEngine, Context, Data, Error, ManyOut, PipelineError, PipelineIO, SegmentSource,
ServiceBackend, ServiceEngine, SingleIn, Source, context,
AsyncTransportEngine, Context, Data, Error, ManyIn, ManyOut, PipelineError, PipelineIO,
SegmentSource, ServiceBackend, ServiceEngine, SingleIn, Source, context,
};
use crate::metrics::MetricsHierarchy;
use crate::metrics::prometheus_names::work_handler;
use crate::protocols::maybe_error::MaybeError;
use ingress::push_handler::WorkHandlerMetrics;
use prometheus::{CounterVec, Histogram, IntCounter, IntCounterVec, IntGauge};

Expand Down Expand Up @@ -94,6 +96,11 @@ pub(crate) struct RequestControlMessage {
/// Reliable for single-machine profiling; treat cross-host values as approximate.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) frontend_send_ts_ns: Option<u64>,
/// For bidirectional dispatch (`request_type == ManyIn`): connection info the
/// worker dials back to in order to receive subsequent request frames. `None`
/// for the unary path, which is the wire-compatible default.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) request_stream_connection_info: Option<ConnectionInfo>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
Expand Down
Loading
Loading