Skip to content

Commit 4cba833

Browse files
authored
fix(mocker): include outputs in mooncake delta prompts (#11060)
Signed-off-by: Yongming Ding <yongmingd@nvidia.com>
1 parent 59358c2 commit 4cba833

6 files changed

Lines changed: 399 additions & 26 deletions

File tree

lib/mocker/src/loadgen/driver.rs

Lines changed: 233 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ use std::cmp::Ordering;
55
use std::collections::BinaryHeap;
66

77
use anyhow::{Context, Result, anyhow, bail};
8+
use rand::rngs::StdRng;
9+
use rand::{Rng, SeedableRng};
810
use rustc_hash::FxHashMap;
911
use uuid::Uuid;
1012

@@ -38,9 +40,12 @@ enum PromptMode {
3840
DeltaCumulative,
3941
}
4042

43+
const SYNTHETIC_DELTA_OUTPUT_SEED: u64 = 0xD37A_0A7E_5EED;
44+
4145
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4246
enum TurnOutcome {
4347
Completed,
48+
Rejected,
4449
Cancelled,
4550
}
4651

@@ -78,6 +83,7 @@ struct TurnRuntime {
7883
struct InFlightTurn {
7984
session_index: usize,
8085
turn_index: usize,
86+
emitted_output_tokens: usize,
8187
}
8288

8389
#[derive(Debug, Clone, Copy)]
@@ -384,6 +390,7 @@ impl WorkloadDriver {
384390
u32::try_from(engine_block_size).context("engine_block_size does not fit in u32")?;
385391
let trace_block_size = trace.block_size;
386392
let is_concurrency = matches!(&policy, SchedulingPolicy::Concurrency(_));
393+
let mut output_rng = StdRng::seed_from_u64(SYNTHETIC_DELTA_OUTPUT_SEED);
387394
let sessions: Vec<SessionRuntime> = trace
388395
.sessions
389396
.into_iter()
@@ -402,12 +409,22 @@ impl WorkloadDriver {
402409
} else {
403410
None
404411
};
412+
let tokens = turn.synthesize_tokens(trace_block_size)?;
413+
let output_token_ids = match turn.output_token_ids {
414+
Some(output_token_ids) => Some(output_token_ids),
415+
None if prompt_mode == PromptMode::DeltaCumulative => Some(
416+
(0..turn.max_output_tokens)
417+
.map(|_| output_rng.random::<u32>())
418+
.collect(),
419+
),
420+
None => None,
421+
};
405422
Ok(TurnRuntime {
406423
request_id: None,
407-
tokens: turn.synthesize_tokens(trace_block_size)?,
424+
tokens,
408425
replay_key: turn.replay_key,
409426
max_output_tokens: turn.max_output_tokens,
410-
output_token_ids: turn.output_token_ids,
427+
output_token_ids,
411428
delay_after_previous_ms: turn.delay_after_previous_ms,
412429
priority: turn.priority,
413430
strict_priority: turn.strict_priority,
@@ -417,7 +434,16 @@ impl WorkloadDriver {
417434
})
418435
.collect::<Result<Vec<_>>>()?;
419436
let cumulative_capacity = if prompt_mode == PromptMode::DeltaCumulative {
420-
turns.iter().map(|turn| turn.tokens.len()).sum()
437+
turns
438+
.iter()
439+
.map(|turn| {
440+
turn.tokens.len()
441+
+ turn
442+
.output_token_ids
443+
.as_ref()
444+
.map_or(0, |output| output.len())
445+
})
446+
.sum()
421447
} else {
422448
0
423449
};
@@ -538,6 +564,7 @@ impl WorkloadDriver {
538564
InFlightTurn {
539565
session_index,
540566
turn_index,
567+
emitted_output_tokens: 0,
541568
},
542569
);
543570
emitted.push(ReadyTurn {
@@ -553,9 +580,60 @@ impl WorkloadDriver {
553580
emitted
554581
}
555582

583+
pub fn on_output_token(&mut self, request_uuid: Uuid, token_id: u32) -> Result<()> {
584+
if self.prompt_mode == PromptMode::Full {
585+
return Ok(());
586+
}
587+
let in_flight = self
588+
.in_flight
589+
.get(&request_uuid)
590+
.copied()
591+
.ok_or_else(|| anyhow!("unknown workload request output for {request_uuid}"))?;
592+
593+
let turn = &self.sessions[in_flight.session_index].turns[in_flight.turn_index];
594+
let planned_output_tokens = turn
595+
.output_token_ids
596+
.as_ref()
597+
.expect("delta turns must have planned output tokens");
598+
let expected_token = planned_output_tokens
599+
.get(in_flight.emitted_output_tokens)
600+
.ok_or_else(|| {
601+
anyhow!(
602+
"workload request {request_uuid} emitted more than {} planned output tokens",
603+
planned_output_tokens.len()
604+
)
605+
})?;
606+
if token_id != *expected_token {
607+
bail!(
608+
"workload request {request_uuid} emitted token {token_id} at position {}, expected {}",
609+
in_flight.emitted_output_tokens,
610+
expected_token
611+
);
612+
}
613+
614+
let in_flight = self
615+
.in_flight
616+
.get_mut(&request_uuid)
617+
.expect("validated in-flight request must still exist");
618+
in_flight.emitted_output_tokens = in_flight
619+
.emitted_output_tokens
620+
.checked_add(1)
621+
.context("workload emitted output token count overflow")?;
622+
Ok(())
623+
}
624+
556625
pub fn on_complete(&mut self, request_uuid: Uuid, now_ms: f64) -> Result<()> {
626+
self.on_terminal(request_uuid, now_ms, false)
627+
}
628+
629+
pub fn on_terminal(&mut self, request_uuid: Uuid, now_ms: f64, rejected: bool) -> Result<()> {
630+
let outcome = if rejected {
631+
TurnOutcome::Rejected
632+
} else {
633+
TurnOutcome::Completed
634+
};
557635
let resolution = self
558-
.resolve_turn(request_uuid, now_ms, TurnOutcome::Completed)?
636+
.resolve_turn(request_uuid, now_ms, outcome)?
559637
.expect("completed turns require an in-flight request");
560638
self.apply_resolution(resolution, now_ms);
561639
Ok(())
@@ -569,7 +647,7 @@ impl WorkloadDriver {
569647
) -> Result<Option<TurnResolution>> {
570648
let Some(in_flight) = self.in_flight.get(&request_uuid).copied() else {
571649
return match outcome {
572-
TurnOutcome::Completed => Err(anyhow!(
650+
TurnOutcome::Completed | TurnOutcome::Rejected => Err(anyhow!(
573651
"unknown workload request completion for {request_uuid}"
574652
)),
575653
TurnOutcome::Cancelled => Ok(None),
@@ -604,8 +682,23 @@ impl WorkloadDriver {
604682
}
605683

606684
let request_id = turn.request_id.clone();
685+
if outcome == TurnOutcome::Rejected && in_flight.emitted_output_tokens != 0 {
686+
bail!(
687+
"rejected workload request {request_uuid} emitted {} output tokens",
688+
in_flight.emitted_output_tokens
689+
);
690+
}
691+
let completed_output_tokens = (outcome == TurnOutcome::Completed
692+
&& self.prompt_mode == PromptMode::DeltaCumulative)
693+
.then(|| {
694+
let planned_output_tokens = turn
695+
.output_token_ids
696+
.as_ref()
697+
.expect("delta turns must have planned output tokens");
698+
planned_output_tokens[..in_flight.emitted_output_tokens].to_vec()
699+
});
607700
let (next_turn_index, next_ready_at_ms, session_ended) = match outcome {
608-
TurnOutcome::Completed => {
701+
TurnOutcome::Completed | TurnOutcome::Rejected => {
609702
let next_turn_index = in_flight
610703
.turn_index
611704
.checked_add(1)
@@ -626,6 +719,11 @@ impl WorkloadDriver {
626719
session.in_flight = None;
627720
session.next_turn_index = next_turn_index;
628721
session.next_ready_at_ms = next_ready_at_ms;
722+
if next_ready_at_ms.is_some()
723+
&& let Some(output_tokens) = completed_output_tokens
724+
{
725+
session.cumulative_tokens.extend(output_tokens);
726+
}
629727
if let Some(ready_at_ms) = next_ready_at_ms {
630728
self.ready_sessions.push(ReadySession {
631729
ready_at_ms,
@@ -1057,7 +1155,7 @@ mod tests {
10571155
}
10581156

10591157
#[test]
1060-
fn accumulating_delta_mode_emits_cumulative_input_tokens() {
1158+
fn accumulating_delta_mode_includes_previous_output_tokens() {
10611159
let trace = Trace {
10621160
block_size: 4,
10631161
sessions: vec![SessionTrace {
@@ -1066,8 +1164,8 @@ mod tests {
10661164
turns: vec![
10671165
TurnTrace {
10681166
input_length: 6,
1069-
max_output_tokens: 1,
1070-
output_token_ids: None,
1167+
max_output_tokens: 2,
1168+
output_token_ids: Some(vec![20, 21]),
10711169
replay_key: None,
10721170
hash_ids: vec![10, 11],
10731171
delay_after_previous_ms: 0.0,
@@ -1094,20 +1192,145 @@ mod tests {
10941192
let first = driver.pop_ready(0.0, usize::MAX);
10951193
assert_eq!(first.len(), 1);
10961194
assert_eq!(first[0].request.tokens, vec![10, 10, 10, 10, 11, 11]);
1195+
assert_eq!(first[0].request.output_token_ids, Some(vec![20, 21]));
10971196
assert_eq!(first[0].request.priority, 3);
10981197
assert_eq!(first[0].request.strict_priority, 4);
1198+
driver.on_output_token(first[0].request_uuid, 20).unwrap();
1199+
driver.on_output_token(first[0].request_uuid, 21).unwrap();
10991200
driver.on_complete(first[0].request_uuid, 10.0).unwrap();
11001201

11011202
let second = driver.pop_ready(15.0, usize::MAX);
11021203
assert_eq!(second.len(), 1);
11031204
assert_eq!(
11041205
second[0].request.tokens,
1105-
vec![10, 10, 10, 10, 11, 11, 12, 12, 12]
1206+
vec![10, 10, 10, 10, 11, 11, 20, 21, 12, 12, 12]
11061207
);
11071208
assert_eq!(second[0].request.priority, -2);
11081209
assert_eq!(second[0].request.strict_priority, 7);
11091210
}
11101211

1212+
#[test]
1213+
fn accumulating_delta_mode_plans_missing_output_token_ids() {
1214+
let trace = Trace {
1215+
block_size: 1,
1216+
sessions: vec![SessionTrace {
1217+
session_id: "a".into(),
1218+
first_arrival_timestamp_ms: Some(0.0),
1219+
turns: vec![
1220+
TurnTrace {
1221+
input_length: 2,
1222+
max_output_tokens: 3,
1223+
hash_ids: vec![10, 11],
1224+
..Default::default()
1225+
},
1226+
TurnTrace {
1227+
input_length: 1,
1228+
max_output_tokens: 1,
1229+
hash_ids: vec![12],
1230+
..Default::default()
1231+
},
1232+
],
1233+
}],
1234+
};
1235+
let mut driver = WorkloadDriver::new_concurrency_accumulating_deltas(trace, 1, 1).unwrap();
1236+
1237+
let first = driver.pop_ready(0.0, usize::MAX);
1238+
assert_eq!(first.len(), 1);
1239+
let planned_output = first[0]
1240+
.request
1241+
.output_token_ids
1242+
.clone()
1243+
.expect("delta replay should plan synthetic outputs");
1244+
assert_eq!(planned_output.len(), 3);
1245+
for &token_id in &planned_output {
1246+
driver
1247+
.on_output_token(first[0].request_uuid, token_id)
1248+
.unwrap();
1249+
}
1250+
driver.on_complete(first[0].request_uuid, 1.0).unwrap();
1251+
1252+
let second = driver.pop_ready(1.0, usize::MAX);
1253+
assert_eq!(second.len(), 1);
1254+
let mut expected = vec![10, 11];
1255+
expected.extend(planned_output);
1256+
expected.push(12);
1257+
assert_eq!(second[0].request.tokens, expected);
1258+
assert_eq!(
1259+
second[0].request.output_token_ids.as_ref().map(Vec::len),
1260+
Some(1)
1261+
);
1262+
}
1263+
1264+
#[test]
1265+
fn accumulating_delta_mode_appends_only_emitted_output_tokens() {
1266+
let trace = Trace {
1267+
block_size: 1,
1268+
sessions: vec![SessionTrace {
1269+
session_id: "a".into(),
1270+
first_arrival_timestamp_ms: Some(0.0),
1271+
turns: vec![
1272+
TurnTrace {
1273+
input_length: 1,
1274+
max_output_tokens: 3,
1275+
output_token_ids: Some(vec![20, 21, 22]),
1276+
hash_ids: vec![10],
1277+
..Default::default()
1278+
},
1279+
TurnTrace {
1280+
input_length: 1,
1281+
max_output_tokens: 1,
1282+
hash_ids: vec![12],
1283+
..Default::default()
1284+
},
1285+
],
1286+
}],
1287+
};
1288+
let mut driver = WorkloadDriver::new_concurrency_accumulating_deltas(trace, 1, 1).unwrap();
1289+
1290+
let first = driver.pop_ready(0.0, usize::MAX);
1291+
driver.on_output_token(first[0].request_uuid, 20).unwrap();
1292+
driver.on_output_token(first[0].request_uuid, 21).unwrap();
1293+
driver.on_complete(first[0].request_uuid, 1.0).unwrap();
1294+
1295+
let second = driver.pop_ready(1.0, usize::MAX);
1296+
assert_eq!(second[0].request.tokens, vec![10, 20, 21, 12]);
1297+
}
1298+
1299+
#[test]
1300+
fn accumulating_delta_mode_does_not_append_rejected_output_tokens() {
1301+
let trace = Trace {
1302+
block_size: 1,
1303+
sessions: vec![SessionTrace {
1304+
session_id: "a".into(),
1305+
first_arrival_timestamp_ms: Some(0.0),
1306+
turns: vec![
1307+
TurnTrace {
1308+
input_length: 1,
1309+
max_output_tokens: 2,
1310+
output_token_ids: Some(vec![20, 21]),
1311+
hash_ids: vec![10],
1312+
..Default::default()
1313+
},
1314+
TurnTrace {
1315+
input_length: 1,
1316+
max_output_tokens: 1,
1317+
hash_ids: vec![12],
1318+
..Default::default()
1319+
},
1320+
],
1321+
}],
1322+
};
1323+
let mut driver = WorkloadDriver::new_concurrency_accumulating_deltas(trace, 1, 1).unwrap();
1324+
1325+
let first = driver.pop_ready(0.0, usize::MAX);
1326+
driver
1327+
.on_terminal(first[0].request_uuid, 1.0, true)
1328+
.unwrap();
1329+
1330+
let second = driver.pop_ready(1.0, usize::MAX);
1331+
assert_eq!(second[0].request.tokens, vec![10, 12]);
1332+
}
1333+
11111334
#[test]
11121335
fn agentic_mode_releases_turn_after_dependency_completion_plus_delay() {
11131336
let trace = AgenticTrace {

lib/mocker/src/loadgen/types.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,11 @@ pub enum DynamoRequestTrace {
5454
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5555
pub enum TraceFileFormat {
5656
Mooncake,
57-
/// Mooncake-shaped rows where follow-up turns contain input deltas.
58-
/// Offline replay accumulates those deltas per session in token space before
59-
/// computing engine block hashes. Use this only for delta traces: it expands
60-
/// compact session deltas into cumulative prompts and can use much more
61-
/// memory than `Mooncake`.
57+
/// Mooncake-shaped rows where follow-up turns contain new input deltas.
58+
/// Offline replay accumulates each generated output and the next input delta
59+
/// per session before computing engine block hashes. Use this only for delta
60+
/// traces: it expands compact session turns into cumulative prompts and can
61+
/// use much more memory than `Mooncake`.
6262
MooncakeDelta,
6363
/// Mooncake request/cache rows plus explicit request-level workflow
6464
/// dependencies. Each row dispatches after `wait_for` completions plus its

0 commit comments

Comments
 (0)