307 lines
10 KiB
Rust
307 lines
10 KiB
Rust
// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
#![allow(dead_code)]
|
|
|
|
use anyhow::Error;
|
|
use futures::{StreamExt, stream};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::{sync::Arc, time::Duration};
|
|
|
|
use dynamo_runtime::engine::ResponseStream;
|
|
use dynamo_runtime::pipeline::{
|
|
AsyncEngine,
|
|
Data,
|
|
Event,
|
|
ManyOut,
|
|
Operator,
|
|
ServiceBackend,
|
|
ServiceEngine,
|
|
ServiceFrontend,
|
|
SingleIn,
|
|
*, // TODO remove the star
|
|
};
|
|
|
|
mod common;
|
|
use common::engines::{AsyncGenerator, LlmdbaEngine as LambdaEngine};
|
|
use common::mock;
|
|
|
|
/// The [`super::engine::ResponseStream`] is annotated with the following types.
|
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
pub enum Annotated<T: Data> {
|
|
/// The primary data which expected to be returned.
|
|
Data(T),
|
|
|
|
/// An actionable [`Event`] that can be handled.
|
|
Event(Event),
|
|
|
|
/// Additional information or metadata produced by the pipeline.
|
|
Comment(String),
|
|
|
|
/// An error produced by the pipeline. Multiple errors can be produced.
|
|
Error(String),
|
|
|
|
/// A sentinel value to indicate the end of the stream. This should not be emitted publicly.
|
|
/// The implementation should be able to do the equivalent of a `.take_while` and trigger a
|
|
/// stop if detected.
|
|
End,
|
|
}
|
|
|
|
/// An [`Operator`] is used when you want to transform both the input and output of a pipeline.
|
|
/// In this case, our operator will perform the preprocessing step, but also add an annotation
|
|
/// to the output stream
|
|
struct PreprocesOperator {}
|
|
|
|
#[async_trait::async_trait]
|
|
impl
|
|
Operator<
|
|
SingleIn<String>,
|
|
ManyOut<Annotated<String>>,
|
|
SingleIn<String>,
|
|
ManyOut<Annotated<String>>,
|
|
> for PreprocesOperator
|
|
{
|
|
async fn generate(
|
|
&self,
|
|
req: SingleIn<String>,
|
|
next: Arc<dyn AsyncEngine<SingleIn<String>, ManyOut<Annotated<String>>, Error>>,
|
|
) -> Result<ManyOut<Annotated<String>>, Error> {
|
|
// capture some details about the request
|
|
let prepend = vec![Annotated::<String>::Comment(format!(
|
|
"PreprocessOperator: {:?}",
|
|
req
|
|
))];
|
|
|
|
// we will append the result of this to the response stream via a chain
|
|
let prepend_stream = stream::iter(prepend);
|
|
|
|
// modify the request
|
|
let req = req.map(|x| format!("{} from operator", x));
|
|
|
|
// issue the preprocessed request to the next engine
|
|
let stream = next.generate(req).await?;
|
|
|
|
// capture the context of the response stream
|
|
let ctx = stream.context();
|
|
|
|
// chain the prepend stream to the response stream
|
|
Ok(ResponseStream::new(
|
|
Box::pin(prepend_stream.chain(stream)),
|
|
ctx,
|
|
))
|
|
}
|
|
}
|
|
|
|
fn make_backend_engine() -> ServiceEngine<SingleIn<String>, ManyOut<Annotated<String>>> {
|
|
LambdaEngine::from_generator(AsyncGenerator::<String, Annotated<String>>::new(
|
|
|(req, stream)| async move {
|
|
let chars = req.chars().collect::<Vec<char>>();
|
|
for c in chars {
|
|
match stream.emit(Annotated::Data(c.to_string())).await {
|
|
Ok(_) => {}
|
|
Err(_) => return,
|
|
}
|
|
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
}
|
|
},
|
|
))
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_service_source_sink() {
|
|
let source = ServiceFrontend::<SingleIn<String>, ManyOut<Annotated<String>>>::new();
|
|
let sink = ServiceBackend::from_engine(make_backend_engine());
|
|
|
|
let service = source.link(sink).unwrap().link(source).unwrap();
|
|
|
|
let mut stream = service.generate("test".to_string().into()).await.unwrap();
|
|
|
|
let mut counter = 0;
|
|
while let Some(_output) = stream.next().await {
|
|
counter += 1;
|
|
}
|
|
|
|
assert_eq!(counter, 4);
|
|
}
|
|
|
|
fn make_preprocessor() -> Arc<PipelineNode<SingleIn<String>, SingleIn<String>>> {
|
|
PipelineNode::<SingleIn<String>, SingleIn<String>>::new(Box::new(|req| {
|
|
Ok(req.map(|x| format!("{} world", x)))
|
|
}))
|
|
}
|
|
|
|
#[allow(clippy::type_complexity)]
|
|
fn make_postprocessor() -> Arc<PipelineNode<ManyOut<Annotated<String>>, ManyOut<Annotated<String>>>>
|
|
{
|
|
PipelineNode::<ManyOut<Annotated<String>>, ManyOut<Annotated<String>>>::new(Box::new(|req| {
|
|
let ctx = req.context();
|
|
let double_stream = req.flat_map(|x| {
|
|
let x1 = x.clone();
|
|
let x2 = x;
|
|
stream::iter(vec![x1, x2])
|
|
});
|
|
Ok(ResponseStream::new(Box::pin(double_stream), ctx))
|
|
}))
|
|
}
|
|
|
|
// Node 0:
|
|
// [frontend] -------[pre processor]-----> [backend]
|
|
// [frontend] <----- [post processor] ---- [backend]
|
|
fn make_service()
|
|
-> Result<ServiceEngine<SingleIn<String>, ManyOut<Annotated<String>>>, PipelineError> {
|
|
// Frontend - Callable interface
|
|
let frontend = ServiceFrontend::<SingleIn<String>, ManyOut<Annotated<String>>>::new();
|
|
|
|
// Mimics processing the prompt and tokenization
|
|
let preprocess = make_preprocessor();
|
|
|
|
// Mimics decoding; shows we can use any type of stream operation,
|
|
// e.g. map, flat_map, fold, scan, etc. to transform the response stream
|
|
let postprocess = make_postprocessor();
|
|
|
|
// Mimics backend streaming by emitting each character of the input string
|
|
let backend = ServiceBackend::from_engine(make_backend_engine());
|
|
|
|
// LLM Pipelines are build by linking the frontend to the backend for input handling
|
|
// then linking from the backend to the frontend for the output handling
|
|
let service = frontend
|
|
.link(preprocess)?
|
|
.link(backend)?
|
|
.link(postprocess)?
|
|
.link(frontend)?;
|
|
|
|
Ok(service)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_service_source_node_sink() {
|
|
let service = make_service().unwrap();
|
|
|
|
let mut stream = service.generate("test".to_string().into()).await.unwrap();
|
|
|
|
let mut counter = 0;
|
|
while let Some(_output) = stream.next().await {
|
|
counter += 1;
|
|
}
|
|
|
|
assert_eq!(counter, 20);
|
|
}
|
|
|
|
// Put the post process on node 0, but the preprocessor and the compute on node1
|
|
// Node 0:
|
|
// [frontend] ---------------------------> [segment_sink]
|
|
// [frontend] <----- [post processor] ---- [segment_sink]
|
|
//
|
|
// Node 1:
|
|
// [segment_source] ---- [preprocessor] ---> [backend]
|
|
// [segment_source] <----------------------- [backend]
|
|
#[tokio::test]
|
|
#[ignore = "Blocked by AsyncEngineStream trait missing Sync supertrait"]
|
|
#[expect(unused_variables)]
|
|
async fn test_disaggregated_service() {
|
|
println!("Running test_disaggregated_service");
|
|
|
|
// Node 0
|
|
let frontend = ServiceFrontend::<SingleIn<String>, ManyOut<Annotated<String>>>::new();
|
|
let postprocessor = make_postprocessor();
|
|
let end_node_0 = SegmentSink::<SingleIn<String>, ManyOut<Annotated<String>>>::new();
|
|
let node0_service = frontend
|
|
.link(end_node_0.clone())
|
|
.unwrap()
|
|
.link(postprocessor)
|
|
.unwrap()
|
|
.link(frontend)
|
|
.unwrap();
|
|
|
|
// Node 1
|
|
let start_node1 = SegmentSource::<SingleIn<String>, ManyOut<Annotated<String>>>::new();
|
|
let preprocessor = make_preprocessor();
|
|
let backend = ServiceBackend::from_engine(make_backend_engine());
|
|
let node1_service = start_node1
|
|
.link(preprocessor)
|
|
.unwrap()
|
|
.link(backend)
|
|
.unwrap()
|
|
.link(start_node1.clone())
|
|
.unwrap();
|
|
|
|
let opts = mock::MockNetworkOptions::default();
|
|
let (egress, ingress) = mock::MockNetworkTransport::<
|
|
SingleIn<String>,
|
|
ManyOut<Annotated<String>>,
|
|
>::new_egress_ingress(opts);
|
|
|
|
// BLOCKED: Cannot attach egress because Engine<T,U,E> = Arc<dyn AsyncEngine<...>>
|
|
// but AsyncEngineStream cannot be Sync (by design), preventing trait object creation
|
|
// end_node_0.attach(egress).unwrap();
|
|
// Commented out since attach is blocked
|
|
// ingress.segment(node1_service).unwrap();
|
|
// tokio::spawn(ingress.execute());
|
|
// let mut stream = node0_service
|
|
// .generate("test".to_string().into())
|
|
// .await
|
|
// .unwrap();
|
|
// let mut counter = 0;
|
|
// while let Some(_output) = stream.next().await {
|
|
// counter += 1;
|
|
// }
|
|
// assert_eq!(counter, 20);
|
|
|
|
println!(
|
|
"Test blocked: SegmentSink::attach requires Arc<dyn AsyncEngine> but AsyncEngineStream cannot be Sync"
|
|
);
|
|
}
|
|
|
|
// Node 0:
|
|
// [frontend] --> [pre processor] --> [operator] ----------------------> [backend]
|
|
// [frontend] <---------------------- [operator] <--[post processor] <-- [backend]
|
|
fn make_service_with_operator()
|
|
-> Result<ServiceEngine<SingleIn<String>, ManyOut<Annotated<String>>>, PipelineError> {
|
|
// Frontend - Callable interface
|
|
let frontend = ServiceFrontend::<SingleIn<String>, ManyOut<Annotated<String>>>::new();
|
|
|
|
// Mimics processing the prompt and tokenization
|
|
let preprocess = make_preprocessor();
|
|
|
|
// Mimics decoding; shows we can use any type of stream operation,
|
|
// e.g. map, flat_map, fold, scan, etc. to transform the response stream
|
|
let postprocess = make_postprocessor();
|
|
|
|
// Mimics backend streaming by emitting each character of the input string
|
|
let backend = ServiceBackend::from_engine(make_backend_engine());
|
|
|
|
let operator = PipelineOperator::new(Arc::new(PreprocesOperator {}));
|
|
|
|
// LLM Pipelines are build by linking the frontend to the backend for input handling
|
|
// then linking from the backend to the frontend for the output handling
|
|
let service = frontend
|
|
.link(preprocess)?
|
|
.link(operator.forward_edge())?
|
|
.link(backend)?
|
|
.link(postprocess)?
|
|
.link(operator.backward_edge())?
|
|
.link(frontend)?;
|
|
|
|
Ok(service)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_service_source_node_sink_with_operator() {
|
|
let service = make_service_with_operator().unwrap();
|
|
|
|
let mut stream = service.generate("test".to_string().into()).await.unwrap();
|
|
|
|
let mut counter = 0;
|
|
let mut annotations_counter = 0;
|
|
while let Some(output) = stream.next().await {
|
|
match output {
|
|
Annotated::Data(_) => counter += 1,
|
|
Annotated::Comment(_) => annotations_counter += 1,
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
assert_eq!(annotations_counter, 1);
|
|
assert_eq!(counter, 48);
|
|
}
|