Skip to main content

aisimulate_core/replay/
replayer.rs

1// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Public replay facade over the mechanically moved topology runtimes.
5
6use std::collections::VecDeque;
7use std::time::Instant;
8
9use anyhow::Result as AnyResult;
10use uuid::Uuid;
11
12use crate::replay::OfflineDisaggReplayConfig;
13use crate::replay::agg::AggRuntimeImpl;
14use crate::replay::artifact::{
15    ReplayArtifactKvEventVisibility, ReplayArtifactSink, ReplayArtifacts,
16};
17use crate::replay::components::{
18    AdmissionQueue, NoReplayMetadata, ReplayAdmissionMetadata, ReplayEngineObservation, ReplayMode,
19};
20use crate::replay::core::round_robin::{AggregatedRoundRobinPlacement, PoolRoundRobinPlacement};
21use crate::replay::core::{NoEngineEvents, PlacementPolicy, WorkerTopology};
22use crate::replay::disagg::DisaggRuntimeImpl;
23use crate::replay::engine::{ReplayEngineConfig, ReplayEngineFactory};
24use crate::replay::error::{placement_boundary, runtime_error, scaling_boundary};
25use crate::replay::loadgen::ReplayRequestPayload;
26use crate::replay::loadgen::WorkloadDriver;
27use crate::replay::protocol::{DirectRequest, ReplayPromptTokenSource, ReplayRequestContext};
28use crate::replay::scaling::ReplayScalingPolicy;
29use crate::replay::{
30    ReplayCaptureOptions, ReplayDeterminism, ReplayError, ReplayReport, ReplayResult, ReplaySpec,
31    ReplayTopology, SlaThresholds, WorkerStage,
32};
33
34/// Runtime composition supplied by the built-in engine stack or a Dynamo
35/// adapter. The adapter owns concrete Router/Planner construction; Replay only
36/// sees the already-neutral placement and scaling contracts.
37pub trait ReplayComposition {
38    type Metadata: ReplayAdmissionMetadata;
39    type Observation: ReplayEngineObservation;
40    type AggregatedPlacement: PlacementPolicy<
41            ReplayRequestPayload,
42            Metadata = Self::Metadata,
43            Observation = <Self::Observation as ReplayEngineObservation>::Batch,
44        >;
45    type DisaggregatedPlacement: PlacementPolicy<
46            ReplayRequestPayload,
47            Metadata = Self::Metadata,
48            Observation = <Self::Observation as ReplayEngineObservation>::Batch,
49        >;
50
51    fn validate_spec(&self, _spec: &ReplaySpec) -> ReplayResult<()> {
52        Ok(())
53    }
54
55    fn create_aggregated_placement(
56        &mut self,
57        dp_size: u32,
58        topology: Vec<WorkerTopology>,
59    ) -> AnyResult<Self::AggregatedPlacement>;
60
61    fn create_disaggregated_placements(
62        &mut self,
63        prefill_dp_size: u32,
64        prefill_topology: Vec<WorkerTopology>,
65        decode_dp_size: u32,
66        decode_topology: Vec<WorkerTopology>,
67    ) -> AnyResult<(Self::DisaggregatedPlacement, Self::DisaggregatedPlacement)>;
68
69    /// Return the run-owned scaling policy, if this composition has one.
70    fn take_scaling_policy(&mut self) -> AnyResult<Option<Box<dyn ReplayScalingPolicy>>> {
71        Ok(None)
72    }
73
74    /// Inform policy construction about explicitly requested deterministic
75    /// selection. Implementations should use
76    /// [`ReplayDeterminism::selector_seed`] and leave normal runs unseeded.
77    fn set_determinism(&mut self, _determinism: ReplayDeterminism) -> ReplayResult<()> {
78        Ok(())
79    }
80}
81
82/// Classifies every fallible callback from a placement policy at the boundary
83/// where the policy enters the otherwise policy-neutral runtime.
84struct PlacementPolicyBoundary<P>(P);
85
86impl<Request, P> PlacementPolicy<Request> for PlacementPolicyBoundary<P>
87where
88    P: PlacementPolicy<Request>,
89{
90    type Metadata = P::Metadata;
91    type Observation = P::Observation;
92
93    fn place(
94        &mut self,
95        request: &Request,
96        metadata: Self::Metadata,
97        session_id: Option<String>,
98        now_ms: f64,
99    ) -> AnyResult<crate::replay::core::PlacementEffects> {
100        self.0
101            .place(request, metadata, session_id, now_ms)
102            .map_err(placement_boundary)
103    }
104
105    fn observe(
106        &mut self,
107        observation: Self::Observation,
108        now_ms: f64,
109    ) -> AnyResult<Vec<crate::replay::core::Placement>> {
110        self.0
111            .observe(observation, now_ms)
112            .map_err(placement_boundary)
113    }
114
115    fn cancel_pending(&mut self, request_id: Uuid) -> bool {
116        self.0.cancel_pending(request_id)
117    }
118
119    fn request_terminal(
120        &mut self,
121        request_id: Uuid,
122        now_ms: f64,
123    ) -> AnyResult<Vec<crate::replay::core::Placement>> {
124        self.0
125            .request_terminal(request_id, now_ms)
126            .map_err(placement_boundary)
127    }
128
129    fn prefill_completed(
130        &mut self,
131        request_id: Uuid,
132        now_ms: f64,
133    ) -> AnyResult<Vec<crate::replay::core::Placement>> {
134        self.0
135            .prefill_completed(request_id, now_ms)
136            .map_err(placement_boundary)
137    }
138
139    fn pending_count(&self) -> usize {
140        self.0.pending_count()
141    }
142
143    fn worker_ready(
144        &mut self,
145        worker: WorkerTopology,
146        now_ms: f64,
147    ) -> AnyResult<Vec<crate::replay::core::Placement>> {
148        self.0
149            .worker_ready(worker, now_ms)
150            .map_err(placement_boundary)
151    }
152
153    fn worker_draining(
154        &mut self,
155        worker: WorkerTopology,
156        now_ms: f64,
157    ) -> AnyResult<Vec<crate::replay::core::Placement>> {
158        self.0
159            .worker_draining(worker, now_ms)
160            .map_err(placement_boundary)
161    }
162
163    fn worker_removed(
164        &mut self,
165        worker: WorkerTopology,
166        now_ms: f64,
167    ) -> AnyResult<Vec<crate::replay::core::Placement>> {
168        self.0
169            .worker_removed(worker, now_ms)
170            .map_err(placement_boundary)
171    }
172
173    fn topology_settled(&mut self, now_ms: f64) -> AnyResult<Vec<crate::replay::core::Placement>> {
174        self.0.topology_settled(now_ms).map_err(placement_boundary)
175    }
176}
177
178/// Classifies Planner/scaling callbacks without exposing policy-specific types
179/// to the aggregated or disaggregated runtime.
180struct ScalingPolicyBoundary(Box<dyn ReplayScalingPolicy>);
181
182impl ReplayScalingPolicy for ScalingPolicyBoundary {
183    fn capture_lifecycle_evidence(&self) -> bool {
184        self.0.capture_lifecycle_evidence()
185    }
186
187    fn initial_tick_ms(&mut self) -> AnyResult<f64> {
188        self.0.initial_tick_ms().map_err(scaling_boundary)
189    }
190
191    fn on_tick(
192        &mut self,
193        snapshot: crate::replay::scaling::ReplayScalingSnapshot,
194    ) -> AnyResult<crate::replay::scaling::ReplayScalingDecision> {
195        self.0.on_tick(snapshot).map_err(scaling_boundary)
196    }
197}
198
199/// Replay-owned runtime input used by compatibility runners that already
200/// lowered a trace into the shared workload driver.
201///
202/// Serializable callers should keep using [`ReplaySpec::requests`]. Dynamo's
203/// legacy entrypoints use this seam to preserve multi-turn, concurrency, and
204/// agentic scheduling without recompiling Replay sources in the Dynamo crate.
205#[doc(hidden)]
206pub enum ReplayRuntimeInput {
207    Requests(VecDeque<DirectRequest>),
208    Workload(WorkloadDriver),
209}
210
211/// Built-in engine-only composition: Round-robin placement and fixed capacity.
212#[derive(Debug, Default, Clone, Copy)]
213pub struct RoundRobinComposition;
214
215impl ReplayComposition for RoundRobinComposition {
216    type Metadata = NoReplayMetadata;
217    type Observation = NoEngineEvents;
218    type AggregatedPlacement = AggregatedRoundRobinPlacement<()>;
219    type DisaggregatedPlacement = PoolRoundRobinPlacement<()>;
220
221    fn validate_spec(&self, spec: &ReplaySpec) -> ReplayResult<()> {
222        if spec.adapters.placement.provider != "round_robin" {
223            return Err(ReplayError::InvalidSpec(format!(
224                "engine composition requires round_robin placement, got {:?}",
225                spec.adapters.placement.provider
226            )));
227        }
228        if spec.adapters.scaling.provider != "none" {
229            return Err(ReplayError::InvalidSpec(format!(
230                "engine composition does not provide scaling, got {:?}",
231                spec.adapters.scaling.provider
232            )));
233        }
234        Ok(())
235    }
236
237    fn create_aggregated_placement(
238        &mut self,
239        dp_size: u32,
240        topology: Vec<WorkerTopology>,
241    ) -> AnyResult<Self::AggregatedPlacement> {
242        Ok(AggregatedRoundRobinPlacement::new(dp_size, topology))
243    }
244
245    fn create_disaggregated_placements(
246        &mut self,
247        _prefill_dp_size: u32,
248        prefill_topology: Vec<WorkerTopology>,
249        _decode_dp_size: u32,
250        decode_topology: Vec<WorkerTopology>,
251    ) -> AnyResult<(Self::DisaggregatedPlacement, Self::DisaggregatedPlacement)> {
252        Ok((
253            PoolRoundRobinPlacement::new(prefill_topology),
254            PoolRoundRobinPlacement::new(decode_topology),
255        ))
256    }
257}
258
259/// Owns one replay execution: canonical spec, engine construction, and
260/// the selected placement/scaling composition.
261pub struct Replayer<C = RoundRobinComposition> {
262    spec: ReplaySpec,
263    factory: ReplayEngineFactory,
264    composition: C,
265    runtime_input: Option<ReplayRuntimeInput>,
266    capture: ReplayCaptureOptions,
267}
268
269impl Replayer<RoundRobinComposition> {
270    pub fn new(spec: ReplaySpec, factory: ReplayEngineFactory) -> ReplayResult<Self> {
271        Self::with_composition(spec, factory, RoundRobinComposition)
272    }
273
274    /// Run a fixed, aggregated single-worker replay and retain detailed
275    /// request/output/native-KV observations from the same Replayer-owned
276    /// aggregated runtime that produces the normal report.
277    ///
278    /// This contract intentionally targets one worker artifact. Multi-worker,
279    /// scaling, and disaggregated runs should consume the normal report and
280    /// placement/scaling observation contracts instead of creating a second
281    /// scheduler loop solely for artifact generation.
282    pub fn run_with_artifacts(
283        self,
284        visibility: ReplayArtifactKvEventVisibility,
285    ) -> ReplayResult<(ReplayReport, ReplayArtifacts)> {
286        let sink = ReplayArtifactSink::new(visibility);
287        let report = self.run_inner(Some(sink.clone()))?;
288        Ok((report, sink.take()?))
289    }
290}
291
292impl<C: ReplayComposition> Replayer<C> {
293    pub fn with_composition(
294        spec: ReplaySpec,
295        factory: ReplayEngineFactory,
296        composition: C,
297    ) -> ReplayResult<Self> {
298        spec.validate()?;
299        composition.validate_spec(&spec)?;
300        Ok(Self {
301            spec,
302            factory,
303            composition,
304            runtime_input: None,
305            capture: ReplayCaptureOptions::default(),
306        })
307    }
308
309    /// Override the serializable request list with an already-lowered,
310    /// Replay-owned runtime input.
311    #[doc(hidden)]
312    pub fn with_runtime_input(mut self, input: ReplayRuntimeInput) -> Self {
313        self.runtime_input = Some(input);
314        self
315    }
316
317    /// Configure detailed capture and canonical determinism for this run.
318    pub fn with_capture_options(mut self, options: ReplayCaptureOptions) -> Self {
319        self.capture = options;
320        self
321    }
322
323    pub fn run(self) -> ReplayResult<ReplayReport> {
324        self.run_inner(None)
325    }
326
327    fn run_inner(
328        mut self,
329        artifact_sink: Option<ReplayArtifactSink>,
330    ) -> ReplayResult<ReplayReport> {
331        let wall_start = Instant::now();
332        self.composition.set_determinism(self.capture.determinism)?;
333        let engine_config = ReplayEngineConfig::parse(&self.spec.engine)?;
334        engine_config.validate_topology(&self.spec.topology)?;
335        let runtime_input = match self.runtime_input.take() {
336            Some(mut input) => {
337                apply_runtime_determinism(&mut input, self.capture.determinism);
338                input
339            }
340            None => {
341                ReplayRuntimeInput::Requests(lower_requests(&self.spec, self.capture.determinism)?)
342            }
343        };
344        let mode = self
345            .spec
346            .max_in_flight
347            .map_or(ReplayMode::Trace, |max_in_flight| ReplayMode::Concurrency {
348                max_in_flight,
349            });
350        let scaling = self
351            .composition
352            .take_scaling_policy()
353            .map_err(|error| ReplayError::Scaling(format!("{error:#}")))?;
354
355        let collector = match &self.spec.topology {
356            ReplayTopology::Aggregated { workers } => {
357                let role_factory = self.factory.role_factory(
358                    &engine_config,
359                    WorkerStage::Aggregated,
360                    C::Observation::capture_engine_kv_events(WorkerStage::Aggregated)
361                        || artifact_sink.is_some(),
362                )?;
363                let startup_time_ms = positive_delay(workers.startup_delay_ms);
364                if artifact_sink.is_some()
365                    && (workers.initial_workers != 1
366                        || role_factory.dp_size() != 1
367                        || scaling.is_some())
368                {
369                    return Err(ReplayError::InvalidSpec(
370                        "detailed replay artifacts require fixed aggregated topology with one logical DP1 worker"
371                            .to_string(),
372                    ));
373                }
374
375                let mut runtime = AggRuntimeImpl::<
376                    PlacementPolicyBoundary<C::AggregatedPlacement>,
377                    C::Observation,
378                    C::Metadata,
379                >::new_composed(
380                    role_factory,
381                    admission_queue(runtime_input, mode),
382                    workers.initial_workers,
383                    startup_time_ms,
384                    |dp_size, topology| {
385                        self.composition
386                            .create_aggregated_placement(dp_size, topology)
387                            .map(PlacementPolicyBoundary)
388                            .map_err(placement_boundary)
389                    },
390                )
391                .map_err(runtime_error)?
392                .with_capture_options(self.capture)
393                .with_per_request_records(
394                    self.spec.record_per_request || self.capture.effective_per_request(),
395                )
396                .with_max_sim_time_ms(self.spec.max_sim_time_ms);
397                if let Some(sink) = artifact_sink {
398                    runtime = runtime.with_artifact_sink(sink);
399                }
400                if let Some(policy) = scaling {
401                    runtime = runtime.with_scaling_policy(Box::new(ScalingPolicyBoundary(policy)));
402                }
403                runtime.run().map_err(runtime_error)?.0
404            }
405            ReplayTopology::Disaggregated {
406                prefill,
407                decode,
408                handoff_latency_ms,
409            } => {
410                if artifact_sink.is_some() {
411                    return Err(ReplayError::InvalidSpec(
412                        "detailed replay artifacts require aggregated topology".to_string(),
413                    ));
414                }
415                let prefill_factory = self.factory.role_factory(
416                    &engine_config,
417                    WorkerStage::Prefill,
418                    C::Observation::capture_engine_kv_events(WorkerStage::Prefill),
419                )?;
420                let decode_factory = self.factory.role_factory(
421                    &engine_config,
422                    WorkerStage::Decode,
423                    C::Observation::capture_engine_kv_events(WorkerStage::Decode),
424                )?;
425                let config = OfflineDisaggReplayConfig {
426                    prefill_factory,
427                    decode_factory,
428                    prefill_startup_time_ms: positive_delay(prefill.startup_delay_ms),
429                    decode_startup_time_ms: positive_delay(decode.startup_delay_ms),
430                    num_prefill_workers: prefill.initial_workers,
431                    num_decode_workers: decode.initial_workers,
432                    handoff_latency_ms: *handoff_latency_ms,
433                };
434                let mut runtime = DisaggRuntimeImpl::<
435                    PlacementPolicyBoundary<C::DisaggregatedPlacement>,
436                    C::Observation,
437                    C::Metadata,
438                >::new_composed(
439                    &config,
440                    admission_queue(runtime_input, mode),
441                    false,
442                    |prefill_dp, prefill_topology, decode_dp, decode_topology| {
443                        self.composition
444                            .create_disaggregated_placements(
445                                prefill_dp,
446                                prefill_topology,
447                                decode_dp,
448                                decode_topology,
449                            )
450                            .map(|(prefill, decode)| {
451                                (
452                                    PlacementPolicyBoundary(prefill),
453                                    PlacementPolicyBoundary(decode),
454                                )
455                            })
456                            .map_err(placement_boundary)
457                    },
458                )
459                .map_err(runtime_error)?
460                .with_capture_options(self.capture)
461                .with_per_request_records(
462                    self.spec.record_per_request || self.capture.effective_per_request(),
463                )
464                .with_max_sim_time_ms(self.spec.max_sim_time_ms);
465                if let Some(policy) = scaling {
466                    runtime = runtime.with_scaling_policy(Box::new(ScalingPolicyBoundary(policy)));
467                }
468                runtime.run().map_err(runtime_error)?.0
469            }
470        };
471
472        Ok(finish_report(collector, self.spec.sla)
473            .with_wall_time_ms(wall_start.elapsed().as_secs_f64() * 1_000.0))
474    }
475}
476
477fn admission_queue<Metadata: ReplayAdmissionMetadata>(
478    input: ReplayRuntimeInput,
479    mode: ReplayMode,
480) -> AdmissionQueue<Metadata> {
481    match input {
482        ReplayRuntimeInput::Requests(requests) => AdmissionQueue::new_requests(requests, mode),
483        ReplayRuntimeInput::Workload(driver) => AdmissionQueue::new_workload(driver, mode),
484    }
485}
486
487fn positive_delay(delay_ms: f64) -> Option<f64> {
488    (delay_ms > 0.0).then_some(delay_ms)
489}
490
491fn lower_requests(
492    spec: &ReplaySpec,
493    determinism: ReplayDeterminism,
494) -> ReplayResult<VecDeque<DirectRequest>> {
495    let mut pending = spec
496        .requests
497        .iter()
498        .enumerate()
499        .map(|(index, request)| -> ReplayResult<_> {
500            let request_id = match determinism {
501                ReplayDeterminism::Random => Uuid::new_v4(),
502                ReplayDeterminism::CanonicalV1 => Uuid::from_u128(
503                    u128::try_from(index)
504                        .expect("usize always fits u128")
505                        .checked_add(1)
506                        .expect("replay request index overflow"),
507                ),
508            };
509            let (tokens, prompt_token_source) = match &request.input_token_ids {
510                Some(tokens) => (tokens.clone(), ReplayPromptTokenSource::Materialized),
511                None => {
512                    let seed = u32::try_from(index)
513                        .unwrap_or(u32::MAX)
514                        .wrapping_mul(1_000_003);
515                    (
516                        (0..request.input_tokens)
517                            .map(|offset| {
518                                seed.wrapping_add(u32::try_from(offset).unwrap_or(u32::MAX))
519                            })
520                            .collect(),
521                        ReplayPromptTokenSource::LengthOnlySynthetic,
522                    )
523                }
524            };
525            let routing = request.routing_metadata()?;
526            Ok(DirectRequest {
527                tokens,
528                max_output_tokens: request.output_tokens,
529                output_token_ids: request.output_token_ids.clone(),
530                uuid: Some(request_id),
531                dp_rank: 0,
532                preferred_dp_rank: request.dp_rank,
533                arrival_timestamp_ms: Some(request.arrival_time_ms),
534                priority: routing.priority,
535                strict_priority: routing.strict_priority,
536                policy_class: routing.policy_class,
537                replay_context: Some(ReplayRequestContext {
538                    authored_id: request.id.clone(),
539                    session_id: request.session_id.clone(),
540                    turn_index: request.turn_index,
541                    metadata: request.metadata.clone(),
542                    prompt_token_source,
543                }),
544            })
545        })
546        .collect::<ReplayResult<Vec<_>>>()?;
547    pending.sort_by(|left, right| {
548        left.arrival_timestamp_ms
549            .expect("ReplaySpec request always has an arrival")
550            .total_cmp(
551                &right
552                    .arrival_timestamp_ms
553                    .expect("ReplaySpec request always has an arrival"),
554            )
555    });
556    Ok(pending.into())
557}
558
559fn apply_runtime_determinism(input: &mut ReplayRuntimeInput, determinism: ReplayDeterminism) {
560    if determinism != ReplayDeterminism::CanonicalV1 {
561        return;
562    }
563    match input {
564        ReplayRuntimeInput::Requests(requests) => {
565            for (index, request) in requests.iter_mut().enumerate() {
566                request.uuid = Some(Uuid::from_u128(
567                    u128::try_from(index)
568                        .expect("usize always fits u128")
569                        .checked_add(1)
570                        .expect("replay request index overflow"),
571                ));
572            }
573        }
574        ReplayRuntimeInput::Workload(driver) => {
575            driver.set_deterministic_request_ids(1);
576        }
577    }
578}
579
580fn finish_report(mut collector: crate::replay::TraceCollector, sla: SlaThresholds) -> ReplayReport {
581    collector.set_sla_thresholds(sla);
582    collector.finish()
583}
584
585#[cfg(test)]
586mod tests {
587    use super::*;
588    use crate::replay::{
589        ProviderSpec, ReplayAdapters, ReplayRequest, ReplayTopology, WorkerPoolSpec,
590    };
591
592    #[test]
593    fn replay_spec_lowering_preserves_correlation_routing_and_prompt_provenance() {
594        let spec = ReplaySpec {
595            version: 1,
596            topology: ReplayTopology::Aggregated {
597                workers: WorkerPoolSpec::default(),
598            },
599            engine: serde_json::Value::Null,
600            adapters: ReplayAdapters {
601                placement: ProviderSpec::round_robin(),
602                scaling: ProviderSpec::no_scaling(),
603            },
604            max_sim_time_ms: None,
605            max_in_flight: None,
606            record_per_request: true,
607            sla: Default::default(),
608            requests: vec![
609                ReplayRequest {
610                    id: "length-only".into(),
611                    arrival_time_ms: 0.0,
612                    input_tokens: 3,
613                    input_token_ids: None,
614                    output_tokens: 2,
615                    output_token_ids: None,
616                    dp_rank: Some(2),
617                    session_id: Some("session-a".into()),
618                    turn_index: Some(4),
619                    metadata: serde_json::json!({
620                        "priority": -7,
621                        "strict_priority": 9,
622                        "policy_class": "latency",
623                        "caller_tag": "preserved"
624                    }),
625                },
626                ReplayRequest {
627                    id: "materialized".into(),
628                    arrival_time_ms: 1.0,
629                    input_tokens: 2,
630                    input_token_ids: Some(vec![41, 42]),
631                    output_tokens: 1,
632                    output_token_ids: None,
633                    dp_rank: None,
634                    session_id: None,
635                    turn_index: None,
636                    metadata: serde_json::Value::Null,
637                },
638            ],
639        };
640
641        let lowered = lower_requests(&spec, ReplayDeterminism::CanonicalV1)
642            .unwrap()
643            .into_iter()
644            .collect::<Vec<_>>();
645        let first = &lowered[0];
646        assert_eq!(first.uuid, Some(Uuid::from_u128(1)));
647        assert_eq!(first.priority, -7);
648        assert_eq!(first.strict_priority, 9);
649        assert_eq!(first.policy_class.as_deref(), Some("latency"));
650        assert_eq!(first.preferred_dp_rank, Some(2));
651        assert!(!first.prompt_tokens_are_placement_safe());
652        let context = first.replay_context.as_ref().unwrap();
653        assert_eq!(context.authored_id, "length-only");
654        assert_eq!(context.session_id.as_deref(), Some("session-a"));
655        assert_eq!(context.turn_index, Some(4));
656        assert_eq!(context.metadata["caller_tag"], "preserved");
657
658        assert_eq!(lowered[1].tokens, vec![41, 42]);
659        assert!(lowered[1].prompt_tokens_are_placement_safe());
660    }
661
662    #[test]
663    fn random_lowering_does_not_use_ordinal_request_uuids() {
664        let spec = ReplaySpec {
665            version: 1,
666            topology: ReplayTopology::aggregated(1),
667            engine: serde_json::Value::Null,
668            adapters: ReplayAdapters::default(),
669            max_sim_time_ms: None,
670            max_in_flight: None,
671            record_per_request: false,
672            sla: Default::default(),
673            requests: vec![ReplayRequest {
674                id: "random".into(),
675                arrival_time_ms: 0.0,
676                input_tokens: 1,
677                input_token_ids: Some(vec![1]),
678                output_tokens: 1,
679                output_token_ids: None,
680                dp_rank: None,
681                session_id: None,
682                turn_index: None,
683                metadata: serde_json::Value::Null,
684            }],
685        };
686
687        let first = lower_requests(&spec, ReplayDeterminism::Random)
688            .unwrap()
689            .pop_front()
690            .unwrap();
691        assert_ne!(first.uuid, Some(Uuid::from_u128(1)));
692    }
693}