-
Notifications
You must be signed in to change notification settings - Fork 749
feat: vllm mocker enhancement #1236
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
41 commits
Select commit
Hold shift + click to select a range
74bc13f
use BTreeSet, and allow for push_front (preemption)
PeaBrane f2343d5
preemption is push_front
PeaBrane 6fe3154
use Hongkuan's quadratic formulas for decode and prefill
PeaBrane cccebad
cleaner scheduling + generation separation, and waterline bug fix
PeaBrane 793d1d1
Merge branch 'main' into rupei/mocker-v0
PeaBrane 394c2bf
restore printing out fwd pass metrics in test
PeaBrane f5ab2e1
Merge remote-tracking branch 'origin/main' into rupei/mocker-v0
PeaBrane dad183f
multi-dp mocker engine
PeaBrane 009ec78
fixed prefill cost, and more conservative watermarking
PeaBrane ee11427
fwd pass metrics
PeaBrane 8e8d0b4
can emit kv event, not tested
PeaBrane e96f810
move block resp test in kv manager
PeaBrane c09f007
basic test passes for both load metrics and kv events
PeaBrane 4502e5e
better tracing
PeaBrane fe20aa3
async engine core
PeaBrane 2fbf998
hook up with dynamo run
PeaBrane b548050
docs
PeaBrane c7c4be5
fmt
PeaBrane 1845a8d
Merge branch 'main' into rupei/mocker-v0
PeaBrane 3ad7780
refactor
PeaBrane c78bef2
works with kv router
PeaBrane a206569
actually load extra mocker args in guide
PeaBrane d3730ff
free blocks if failed to send (receiver dropped)
PeaBrane 68d822a
do not regenereate tokens after pre-emption
PeaBrane d69edcf
evictor cleanup
PeaBrane c08f9ea
only need runtime in dynamic arms
PeaBrane dee1413
no separate extra-mocker-args
PeaBrane 082bcec
Merge branch 'main' into rupei/mocker-v0
PeaBrane 99fd3f2
update to match batched tokens
PeaBrane 85c7ccf
max-num-seqs
PeaBrane ec1f360
enable_prefix_caching arg
PeaBrane 94abc0d
only publish kv events if enable_prefix_caching set true
PeaBrane 35da284
small note on chunked prefill being false for now
PeaBrane c7c072d
revert flags
PeaBrane de54247
revert dynamo-run changes
PeaBrane 81c12aa
tiny reversion
PeaBrane b959df4
another reversion
PeaBrane f07e28d
Merge remote-tracking branch 'origin/main' into rupei/mocker-v0
PeaBrane b15070a
usize reversion
PeaBrane 3a20b9d
clippy
PeaBrane c747606
more clippy
PeaBrane File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
multi-dp mocker engine
- Loading branch information
commit dad183f81503c973b31c96dee3c75435b2e82747
There are no files selected for viewing
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,251 @@ | ||
| // SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| //! MockSchedulerEngine - AsyncEngine wrapper around the Scheduler | ||
| //! | ||
| //! This module provides an AsyncEngine implementation that wraps the Scheduler | ||
| //! to provide streaming token generation with realistic timing simulation. | ||
|
|
||
| use crate::mocker::protocols::{DirectRequest, MockEngineArgs, OutputSignal}; | ||
| use crate::mocker::scheduler::Scheduler; | ||
|
|
||
| use dynamo_runtime::{ | ||
| engine::AsyncEngineContextProvider, | ||
| pipeline::{async_trait, AsyncEngine, Error, ManyOut, ResponseStream, SingleIn}, | ||
| protocols::annotated::Annotated, | ||
| }; | ||
|
|
||
| use rand::Rng; | ||
| use std::collections::HashMap; | ||
| use std::sync::Arc; | ||
| use tokio::sync::{mpsc, Mutex}; | ||
| use tokio_stream::wrappers::ReceiverStream; | ||
| use uuid::Uuid; | ||
|
|
||
| /// Generate a random printable character | ||
| fn generate_random_char() -> String { | ||
| let mut rng = rand::rng(); | ||
| let selection = match rng.random_range(0..4) { | ||
| 0 => ('a'..='z').nth(rng.random_range(0..26)).unwrap(), // lowercase | ||
| 1 => ('A'..='Z').nth(rng.random_range(0..26)).unwrap(), // uppercase | ||
| 2 => ('0'..='9').nth(rng.random_range(0..10)).unwrap(), // digits | ||
| _ => [' ', '.', ',', '!', '?'][rng.random_range(0..5)], // punctuation/space | ||
| }; | ||
| selection.to_string() | ||
| } | ||
|
|
||
| /// AsyncEngine wrapper around the Scheduler that generates random character tokens | ||
| pub struct MockVllmEngine { | ||
| schedulers: Vec<Scheduler>, | ||
| active_requests: Arc<Mutex<HashMap<Uuid, mpsc::Sender<OutputSignal>>>>, | ||
| dp_size: u32, | ||
| } | ||
|
|
||
| impl MockVllmEngine { | ||
| /// Create a new MockVllmEngine with the given parameters | ||
| pub fn new(args: MockEngineArgs) -> Self { | ||
| let mut schedulers = Vec::new(); | ||
| let active_requests = Arc::new(Mutex::new( | ||
| HashMap::<Uuid, mpsc::Sender<OutputSignal>>::new(), | ||
| )); | ||
|
|
||
| // Create multiple schedulers and their background tasks | ||
| for _ in 0..args.dp_size { | ||
| // Create a shared output channel that this scheduler will use | ||
| let (output_tx, output_rx) = mpsc::channel::<OutputSignal>(1024); | ||
|
|
||
| let scheduler = Scheduler::new( | ||
| args.clone(), | ||
| Some(output_tx), | ||
| None, // No global cancellation token | ||
| ); | ||
|
|
||
| schedulers.push(scheduler); | ||
|
|
||
| // Spawn a background task for this scheduler to distribute token notifications to active requests | ||
| let output_rx = Arc::new(Mutex::new(output_rx)); | ||
| let active_requests_clone = active_requests.clone(); | ||
|
|
||
| tokio::spawn(async move { | ||
| loop { | ||
| let signal = { | ||
| let mut rx = output_rx.lock().await; | ||
| match rx.recv().await { | ||
| Some(signal) => signal, | ||
| None => break, // Channel closed | ||
| } | ||
| }; | ||
|
|
||
| // Notify the specific request that a token was generated | ||
| let active = active_requests_clone.lock().await; | ||
| if let Some(request_tx) = active.get(&signal.uuid) { | ||
| let _ = request_tx.send(signal).await; | ||
| } | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| Self { | ||
| schedulers, | ||
| active_requests, | ||
| dp_size: args.dp_size, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[async_trait] | ||
| impl AsyncEngine<SingleIn<DirectRequest>, ManyOut<Annotated<String>>, Error> for MockVllmEngine { | ||
| async fn generate( | ||
| &self, | ||
| input: SingleIn<DirectRequest>, | ||
| ) -> Result<ManyOut<Annotated<String>>, Error> { | ||
| let (mut request, ctx) = input.into_parts(); | ||
|
|
||
| let dp_rank = request.dp_rank.unwrap_or(0); | ||
|
|
||
| // Validate dp_rank | ||
| if dp_rank >= self.dp_size { | ||
| return Err(Error::msg(format!( | ||
| "dp_rank {} is out of bounds for dp_size {}", | ||
| dp_rank, self.dp_size | ||
| ))); | ||
| } | ||
|
|
||
| let request_uuid = ctx.id().parse().unwrap_or(Uuid::new_v4()); | ||
| request.uuid = Some(request_uuid); | ||
|
|
||
| let (request_tx, mut request_rx) = mpsc::channel::<OutputSignal>(64); | ||
| { | ||
| let mut active = self.active_requests.lock().await; | ||
| active.insert(request_uuid, request_tx); | ||
| } | ||
|
|
||
| // Send the request to the appropriate scheduler based on dp_rank | ||
| self.schedulers[dp_rank as usize] | ||
| .receive(request.clone()) | ||
| .await; | ||
|
|
||
| // Create a simple channel for the stream | ||
| let (stream_tx, stream_rx) = mpsc::channel::<Annotated<String>>(64); | ||
|
|
||
| let active_requests = self.active_requests.clone(); | ||
| let async_context = ctx.context(); | ||
|
|
||
| // Spawn a task to handle the complex async logic | ||
| tokio::spawn(async move { | ||
| loop { | ||
| tokio::select! { | ||
| Some(signal) = request_rx.recv() => { | ||
| if signal.completed { | ||
| break; | ||
| } | ||
| let output = generate_random_char(); | ||
| if stream_tx.send(Annotated::from_data(output)).await.is_err() { | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| _ = async_context.stopped() => { | ||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Clean up: remove this request from active requests | ||
| let mut active = active_requests.lock().await; | ||
| active.remove(&request_uuid); | ||
| }); | ||
|
|
||
| // Create a simple ReceiverStream which is naturally Send + Sync | ||
| let stream = ReceiverStream::new(stream_rx); | ||
| Ok(ResponseStream::new(Box::pin(stream), ctx.context())) | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
| use dynamo_runtime::pipeline::Context; | ||
| use futures::StreamExt; | ||
|
|
||
| #[tokio::test] | ||
| async fn test_multiple_workers_with_token_limit() { | ||
| const DP_SIZE: u32 = 2; | ||
| const TOKENS_PER_REQUEST: usize = 20; | ||
|
|
||
| // Create the MockVllmEngine using builder pattern | ||
| let args = MockEngineArgs::builder() | ||
| .speedup_ratio(10.0) | ||
| .dp_size(DP_SIZE) | ||
| .build() | ||
| .unwrap(); | ||
|
|
||
| let engine = MockVllmEngine::new(args); | ||
|
|
||
| // Create 4 DirectRequests: 2 for worker 0, 2 for worker 1 | ||
| let requests = vec![ | ||
| DirectRequest { | ||
| tokens: vec![1, 2, 3, 4], | ||
| max_output_tokens: TOKENS_PER_REQUEST, | ||
| uuid: None, | ||
| dp_rank: Some(0), | ||
| }, | ||
| DirectRequest { | ||
| tokens: vec![5, 6, 7, 8], | ||
| max_output_tokens: TOKENS_PER_REQUEST, | ||
| uuid: None, | ||
| dp_rank: Some(0), | ||
| }, | ||
| DirectRequest { | ||
| tokens: vec![9, 10, 11, 12], | ||
| max_output_tokens: TOKENS_PER_REQUEST, | ||
| uuid: None, | ||
| dp_rank: Some(1), | ||
| }, | ||
| DirectRequest { | ||
| tokens: vec![13, 14, 15, 16], | ||
| max_output_tokens: TOKENS_PER_REQUEST, | ||
| uuid: None, | ||
| dp_rank: Some(1), | ||
| }, | ||
| ]; | ||
|
|
||
| // Generate streams and collect all tokens from each | ||
| for request in requests { | ||
| let ctx = Context::new(request); | ||
| let stream = engine.generate(ctx).await.unwrap(); | ||
|
|
||
| let tokens: Vec<_> = stream.collect().await; | ||
|
|
||
| // Verify each stream produces exactly the expected number of tokens | ||
| assert_eq!(tokens.len(), TOKENS_PER_REQUEST); | ||
|
|
||
| // Verify all tokens contain valid data | ||
| for token in tokens { | ||
| assert!(token.data.is_some()); | ||
| } | ||
| } | ||
|
|
||
| // Give a small delay to ensure cleanup tasks complete | ||
| tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; | ||
|
|
||
| // Verify that active_requests is empty (all requests cleaned up) | ||
| let active_requests = engine.active_requests.lock().await; | ||
| assert!( | ||
| active_requests.is_empty(), | ||
| "Active requests should be empty after streams complete" | ||
| ); | ||
| } | ||
| } | ||
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
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.