Skip to main content

dynamo_mocker/common/
sequence.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::common::protocols::MoveBlock;
5use derive_getters::Getters;
6use dynamo_tokens::blocks::UniqueBlock;
7use dynamo_tokens::{
8    BlockHash, PositionalLineageHash, SaltHash, TokenBlockSequence, Tokens,
9    compute_block_hash_for_tokens, compute_next_sequence_hash,
10};
11use rand::random;
12use validator::Validate;
13
14const MOCKER_SALT_HASH: SaltHash = 1337;
15
16#[derive(Debug)]
17struct FlatTokens {
18    retained: Vec<u32>,
19    retained_start: usize,
20}
21
22impl FlatTokens {
23    fn new(
24        mut tokens: Vec<u32>,
25        output_capacity_hint: usize,
26        block_size: usize,
27        retain_history: bool,
28    ) -> Self {
29        if retain_history {
30            tokens.reserve_exact(output_capacity_hint);
31            return Self {
32                retained: tokens,
33                retained_start: 0,
34            };
35        }
36
37        let retained_start = tokens.len() - (tokens.len() % block_size);
38        // Lazy promotion runs after the next block's first token is pushed, so
39        // the retained window must hold one complete block plus that token.
40        let mut retained = Vec::with_capacity(
41            block_size
42                .checked_add(1)
43                .expect("flat token tail capacity overflow"),
44        );
45        retained.extend_from_slice(&tokens[retained_start..]);
46        Self {
47            retained,
48            retained_start,
49        }
50    }
51
52    fn len(&self) -> usize {
53        self.retained_start
54            .checked_add(self.retained.len())
55            .expect("flat token length overflow")
56    }
57
58    fn push(&mut self, token: u32) {
59        self.retained.push(token);
60    }
61
62    fn pop(&mut self) -> Option<u32> {
63        self.retained.pop()
64    }
65
66    fn complete_block(&self, position: usize, block_size: usize) -> Option<&[u32]> {
67        let start = position.checked_mul(block_size)?;
68        debug_assert!(
69            start >= self.retained_start,
70            "promoted block precedes retained flat-token window"
71        );
72        let end = start.checked_add(block_size)?;
73        let local_start = start.checked_sub(self.retained_start)?;
74        let local_end = end.checked_sub(self.retained_start)?;
75        self.retained.get(local_start..local_end)
76    }
77
78    fn discard_through(&mut self, absolute_end: usize) {
79        let local_end = absolute_end
80            .checked_sub(self.retained_start)
81            .expect("promoted block precedes retained flat-token window");
82        assert!(
83            local_end <= self.retained.len(),
84            "promoted block extends beyond retained flat-token window"
85        );
86        self.retained.drain(..local_end);
87        self.retained_start = absolute_end;
88    }
89}
90
91#[derive(Debug)]
92enum SequenceTokens {
93    Legacy(TokenBlockSequence),
94    Flat(FlatTokens),
95}
96
97/// Create unique blocks, block hashes, and positional-lineage hashes from a
98/// [`TokenBlockSequence`].
99fn create_sequence_cache(
100    tokens: &TokenBlockSequence,
101    block_size: usize,
102    enable_prefix_caching: bool,
103) -> (Vec<UniqueBlock>, Vec<BlockHash>, Vec<PositionalLineageHash>) {
104    let mut unique_blocks = Vec::with_capacity(tokens.blocks().len() + 1);
105    let mut block_hashes = Vec::new();
106    if enable_prefix_caching {
107        block_hashes.reserve(tokens.blocks().len());
108    }
109    let mut plhs = Vec::with_capacity(tokens.blocks().len());
110
111    for (pos, block) in tokens.blocks().iter().enumerate() {
112        if enable_prefix_caching {
113            block_hashes.push(block.block_hash());
114            unique_blocks.push(UniqueBlock::FullBlock(block.sequence_hash()));
115            plhs.push(block.positional_lineage_hash());
116        } else {
117            unique_blocks.push(UniqueBlock::FullBlock(random::<u64>()));
118            plhs.push(PositionalLineageHash::new(
119                random::<u64>(),
120                None,
121                pos as u64,
122            ));
123        }
124    }
125
126    // Only push the partial block if tokens count isn't a multiple of block_size
127    if !tokens.total_tokens().is_multiple_of(block_size) {
128        unique_blocks.push(UniqueBlock::default());
129    }
130    (unique_blocks, block_hashes, plhs)
131}
132
133/// Build the native-G1 block metadata directly over a flat token vector.
134fn create_flat_sequence_cache(
135    tokens: &[u32],
136    block_size: usize,
137    enable_prefix_caching: bool,
138    completion_blocks: usize,
139) -> (Vec<UniqueBlock>, Vec<BlockHash>, Vec<PositionalLineageHash>) {
140    let mut unique_blocks = Vec::with_capacity(completion_blocks);
141    let mut block_hashes = if enable_prefix_caching {
142        Vec::with_capacity(completion_blocks)
143    } else {
144        Vec::new()
145    };
146    let mut plhs = Vec::with_capacity(completion_blocks);
147    let mut parent_hash = None;
148
149    for (position, block) in tokens.chunks_exact(block_size).enumerate() {
150        if enable_prefix_caching {
151            let block_hash = compute_block_hash_for_tokens(block, MOCKER_SALT_HASH);
152            let sequence_hash = parent_hash
153                .map(|parent| compute_next_sequence_hash(parent, block_hash))
154                .unwrap_or(block_hash);
155            block_hashes.push(block_hash);
156            unique_blocks.push(UniqueBlock::FullBlock(sequence_hash));
157            plhs.push(PositionalLineageHash::new(
158                sequence_hash,
159                parent_hash,
160                position as u64,
161            ));
162            parent_hash = Some(sequence_hash);
163        } else {
164            unique_blocks.push(UniqueBlock::FullBlock(random::<u64>()));
165            plhs.push(PositionalLineageHash::new(
166                random::<u64>(),
167                None,
168                position as u64,
169            ));
170        }
171    }
172
173    if !tokens.len().is_multiple_of(block_size) {
174        unique_blocks.push(UniqueBlock::default());
175    }
176
177    (unique_blocks, block_hashes, plhs)
178}
179
180/// A sequence that is actively being built, with the ability to add tokens and commit to hashes
181/// TODO: reuse tokens
182#[derive(Debug, Getters, Validate)]
183pub struct ActiveSequence {
184    unique_blocks: Vec<UniqueBlock>,
185    block_hashes: Vec<BlockHash>,
186    plhs: Vec<PositionalLineageHash>,
187
188    #[getter(skip)]
189    tokens: SequenceTokens,
190
191    #[getter(copy)]
192    #[validate(range(min = 2))]
193    block_size: usize,
194
195    #[getter(copy)]
196    max_output_tokens: usize,
197
198    #[getter(copy)]
199    generated_tokens: usize,
200
201    planned_output_ids: Option<Vec<u32>>,
202
203    #[getter(copy)]
204    num_input_tokens: usize,
205
206    #[getter(copy)]
207    num_allocated_tokens: usize,
208
209    #[getter(copy)]
210    enable_prefix_caching: bool,
211
212    #[getter(copy)]
213    emit_token_ids: bool,
214}
215
216impl ActiveSequence {
217    /// Promote the mutable tail after its last token has actually been
218    /// computed.
219    ///
220    /// A generated block is represented as partial until the scheduler has
221    /// computed every token in it.  The historical path promotes that block
222    /// when the first token of the following block is appended.  Native vLLM
223    /// prefix caching needs the earlier boundary: `allocate_slots()` caches a
224    /// just-completed block before considering the next waiting request in the
225    /// same scheduling pass.
226    pub(crate) fn promote_computed_tail(
227        &mut self,
228        cumulative_computed_tokens: usize,
229    ) -> Option<MoveBlock> {
230        if cumulative_computed_tokens == 0
231            || cumulative_computed_tokens != self.len()
232            || !cumulative_computed_tokens.is_multiple_of(self.block_size)
233        {
234            return None;
235        }
236        self.promote_last_partial()
237    }
238
239    fn promote_last_partial(&mut self) -> Option<MoveBlock> {
240        let UniqueBlock::PartialBlock(uuid) = self.unique_blocks.last().cloned()? else {
241            return None;
242        };
243
244        let parent_hash = self.unique_blocks[..self.unique_blocks.len() - 1]
245            .last()
246            .map(|block| match block {
247                UniqueBlock::FullBlock(hash) => *hash,
248                UniqueBlock::PartialBlock(_) => panic!("partial block cannot be a parent"),
249            });
250        let position = self.plhs.len();
251        debug_assert_eq!(position + 1, self.len() / self.block_size);
252        let (last_seq_hash, last_block_hash, last_plh, promote_token_ids) = match &self.tokens {
253            SequenceTokens::Legacy(tokens) => {
254                let last_complete = tokens.last_complete_block().unwrap_or_else(|| {
255                    panic!(
256                        "partial sequence tail cannot be promoted without a complete token block"
257                    )
258                });
259                let last_seq_hash = if self.enable_prefix_caching {
260                    last_complete.sequence_hash()
261                } else {
262                    random::<u64>()
263                };
264                let last_block_hash = self
265                    .enable_prefix_caching
266                    .then(|| last_complete.block_hash());
267                // With prefix caching off, the sequence hash and PLH must both remain
268                // request-unique so another identical prompt cannot reuse this slot.
269                let last_plh = if self.enable_prefix_caching {
270                    last_complete.positional_lineage_hash()
271                } else {
272                    PositionalLineageHash::new(random::<u64>(), None, position as u64)
273                };
274                let promote_token_ids = if self.emit_token_ids {
275                    Some(last_complete.tokens().to_vec())
276                } else {
277                    None
278                };
279                (last_seq_hash, last_block_hash, last_plh, promote_token_ids)
280            }
281            SequenceTokens::Flat(tokens) => {
282                let complete = tokens
283                    .complete_block(position, self.block_size)
284                    .unwrap_or_else(|| {
285                    panic!(
286                        "partial flat sequence tail cannot be promoted without a complete token block"
287                    )
288                });
289                let last_block_hash = self
290                    .enable_prefix_caching
291                    .then(|| compute_block_hash_for_tokens(complete, MOCKER_SALT_HASH));
292                let last_seq_hash = last_block_hash
293                    .map(|block_hash| {
294                        parent_hash
295                            .map(|parent| compute_next_sequence_hash(parent, block_hash))
296                            .unwrap_or(block_hash)
297                    })
298                    .unwrap_or_else(random::<u64>);
299                let last_plh = if self.enable_prefix_caching {
300                    PositionalLineageHash::new(last_seq_hash, parent_hash, position as u64)
301                } else {
302                    PositionalLineageHash::new(random::<u64>(), None, position as u64)
303                };
304                let promote_token_ids = self.emit_token_ids.then(|| complete.to_vec());
305                (last_seq_hash, last_block_hash, last_plh, promote_token_ids)
306            }
307        };
308        if let Some(last_block_hash) = last_block_hash {
309            self.block_hashes.push(last_block_hash);
310        }
311        self.plhs.push(last_plh);
312        self.unique_blocks.pop();
313
314        self.unique_blocks
315            .push(UniqueBlock::FullBlock(last_seq_hash));
316
317        if !self.emit_token_ids {
318            let promoted_end = position
319                .checked_add(1)
320                .and_then(|blocks| blocks.checked_mul(self.block_size))
321                .expect("promoted flat-token boundary overflow");
322            if let SequenceTokens::Flat(tokens) = &mut self.tokens {
323                tokens.discard_through(promoted_end);
324            }
325        }
326        self.debug_assert_flat_token_invariants();
327
328        Some(MoveBlock::Promote(
329            uuid,
330            last_seq_hash,
331            parent_hash,
332            last_block_hash,
333            last_plh,
334            promote_token_ids,
335        ))
336    }
337
338    /// Create a new ActiveSequence instance with the provided tokens
339    pub fn new(
340        tokens: Vec<u32>,
341        max_output_tokens: usize,
342        block_size: Option<usize>,
343        enable_prefix_caching: bool,
344        emit_token_ids: bool,
345    ) -> Self {
346        Self::new_with_planned_output_ids(
347            tokens,
348            max_output_tokens,
349            block_size,
350            enable_prefix_caching,
351            emit_token_ids,
352            None,
353        )
354    }
355
356    pub fn new_with_planned_output_ids(
357        tokens: Vec<u32>,
358        max_output_tokens: usize,
359        block_size: Option<usize>,
360        enable_prefix_caching: bool,
361        emit_token_ids: bool,
362        planned_output_ids: Option<Vec<u32>>,
363    ) -> Self {
364        let block_size = block_size.unwrap_or(64);
365        let num_input_tokens = tokens.len();
366
367        let tokens = Tokens::from(tokens).into_sequence(block_size as u32, Some(MOCKER_SALT_HASH));
368        let (unique_blocks, block_hashes, plhs) =
369            create_sequence_cache(&tokens, block_size, enable_prefix_caching);
370
371        let seq = Self {
372            unique_blocks,
373            block_hashes,
374            plhs,
375            tokens: SequenceTokens::Legacy(tokens),
376            block_size,
377            max_output_tokens,
378            generated_tokens: 0,
379            planned_output_ids,
380            num_input_tokens,
381            num_allocated_tokens: 0,
382            enable_prefix_caching,
383            emit_token_ids: emit_token_ids && enable_prefix_caching,
384        };
385        seq.validate().expect("invalid ActiveSequence");
386        seq
387    }
388
389    /// Build a native-G1 sequence directly over the request's flat token vector.
390    ///
391    /// `output_capacity_hint` controls eager allocation only. The logical
392    /// generation limit remains `max_output_tokens`, and the vectors can grow
393    /// if the scheduler realizes more output than the hint.
394    pub(crate) fn new_flat_with_planned_output_ids(
395        tokens: Vec<u32>,
396        max_output_tokens: usize,
397        output_capacity_hint: usize,
398        block_size: usize,
399        enable_prefix_caching: bool,
400        emit_token_ids: bool,
401        planned_output_ids: Option<Vec<u32>>,
402    ) -> Self {
403        let num_input_tokens = tokens.len();
404        let emit_token_ids = emit_token_ids && enable_prefix_caching;
405        let output_capacity_hint = output_capacity_hint.min(max_output_tokens);
406        let completion_blocks = num_input_tokens
407            .checked_add(output_capacity_hint)
408            .expect("native sequence completion length overflow")
409            .div_ceil(block_size);
410        let (unique_blocks, block_hashes, plhs) = create_flat_sequence_cache(
411            &tokens,
412            block_size,
413            enable_prefix_caching,
414            completion_blocks,
415        );
416        let tokens = FlatTokens::new(tokens, output_capacity_hint, block_size, emit_token_ids);
417
418        let seq = Self {
419            unique_blocks,
420            block_hashes,
421            plhs,
422            tokens: SequenceTokens::Flat(tokens),
423            block_size,
424            max_output_tokens,
425            generated_tokens: 0,
426            planned_output_ids,
427            num_input_tokens,
428            num_allocated_tokens: 0,
429            enable_prefix_caching,
430            emit_token_ids,
431        };
432        seq.validate().expect("invalid flat ActiveSequence");
433        seq.debug_assert_flat_token_invariants();
434        seq
435    }
436
437    pub fn extra_tokens(&self) -> u32 {
438        (self.len() % self.block_size) as u32
439    }
440
441    pub fn len(&self) -> usize {
442        match &self.tokens {
443            SequenceTokens::Legacy(tokens) => tokens.total_tokens(),
444            SequenceTokens::Flat(tokens) => tokens.len(),
445        }
446    }
447
448    pub fn is_empty(&self) -> bool {
449        self.len() == 0
450    }
451
452    /// Current known sequence footprint in blocks: prompt plus generated tokens.
453    pub(crate) fn current_known_blocks(&self) -> usize {
454        self.len().div_ceil(self.block_size)
455    }
456
457    /// To-completion footprint in blocks: `ceil((prompt + max_output) / block_size)`.
458    ///
459    /// The full physical residency a request needs to run end to end, with no
460    /// prefix-reuse or already-allocated discount. Callers deciding "can it be
461    /// admitted now?" apply their own discounts on top of this primitive.
462    pub(crate) fn to_completion_blocks(&self) -> usize {
463        (self.num_input_tokens + self.max_output_tokens).div_ceil(self.block_size)
464    }
465
466    /// Build a `MoveBlock::Use` signal for blocks up to `cumulative_tokens`
467    /// without updating internal state. Returns `None` if no new blocks are needed.
468    /// Call `commit_allocation` after the signal is successfully processed.
469    pub fn prepare_allocation(&self, cumulative_tokens: usize) -> Option<MoveBlock> {
470        let prev_blocks = self
471            .num_allocated_tokens
472            .div_ceil(self.block_size)
473            .min(self.unique_blocks.len());
474        let target_blocks = cumulative_tokens
475            .div_ceil(self.block_size)
476            .min(self.unique_blocks.len());
477        if target_blocks <= prev_blocks {
478            return None;
479        }
480
481        let range = prev_blocks..target_blocks;
482        let blocks = self.unique_blocks[range.clone()].to_vec();
483
484        let hash_start = prev_blocks.min(self.block_hashes.len());
485        let hash_end = target_blocks.min(self.block_hashes.len());
486        let hashes = self.block_hashes[hash_start..hash_end].to_vec();
487        // Cached per-sequence PLHs (stable across calls).
488        let plh_start = prev_blocks.min(self.plhs.len());
489        let plh_end = target_blocks.min(self.plhs.len());
490        let plhs = self.plhs[plh_start..plh_end].to_vec();
491
492        let token_ids = if self.emit_token_ids && hash_start < hash_end {
493            Some(self.block_token_ids_in(hash_start, hash_end))
494        } else {
495            None
496        };
497
498        let parent = if prev_blocks > 0 {
499            Some(self.unique_blocks[prev_blocks - 1].clone())
500        } else {
501            None
502        };
503        Some(MoveBlock::Use(blocks, hashes, plhs, token_ids, parent))
504    }
505
506    /// Positional lineage hashes for all fully-tokenised blocks in the sequence.
507    /// Mirrors `block_hashes()` but returns the PLH identity used by kvbm-logical.
508    pub fn positional_lineage_hashes(&self) -> &[PositionalLineageHash] {
509        &self.plhs
510    }
511
512    fn block_token_ids_in(&self, start: usize, end: usize) -> Vec<Vec<u32>> {
513        match &self.tokens {
514            SequenceTokens::Legacy(tokens) => tokens.blocks()[start..end]
515                .iter()
516                .map(|block| block.tokens().to_vec())
517                .collect(),
518            SequenceTokens::Flat(tokens) => {
519                assert!(
520                    self.emit_token_ids && tokens.retained_start == 0,
521                    "flat sequences retain full token history only when token-ID events are enabled"
522                );
523                tokens
524                    .retained
525                    .chunks_exact(self.block_size)
526                    .skip(start)
527                    .take(end - start)
528                    .map(<[u32]>::to_vec)
529                    .collect()
530            }
531        }
532    }
533
534    /// Materialize every complete block's token IDs.
535    ///
536    /// # Panics
537    ///
538    /// Panics for native flat sequences that were created without token-ID
539    /// event emission, because those sequences intentionally discard completed
540    /// prompt and decode blocks.
541    pub fn block_token_ids(&self) -> Vec<Vec<u32>> {
542        self.block_token_ids_in(0, self.len() / self.block_size)
543    }
544
545    /// Commit a successful allocation by advancing `num_allocated_tokens`.
546    pub fn commit_allocation(&mut self, cumulative_tokens: usize) {
547        self.num_allocated_tokens = cumulative_tokens;
548    }
549
550    /// Prepare + commit in one call (convenience for paths where failure is impossible).
551    pub fn allocate_blocks_for_chunk(&mut self, cumulative_tokens: usize) -> Option<MoveBlock> {
552        let signal = self.prepare_allocation(cumulative_tokens);
553        self.commit_allocation(cumulative_tokens);
554        signal
555    }
556
557    /// Allocate all remaining blocks at once (backward compat).
558    pub fn take_creation_signal(&mut self) -> Option<MoveBlock> {
559        self.allocate_blocks_for_chunk(self.len())
560    }
561
562    /// Create a new ActiveSequence instance and return the creation signal
563    pub fn new_with_signal(
564        tokens: Vec<u32>,
565        max_output_tokens: usize,
566        block_size: Option<usize>,
567        enable_prefix_caching: bool,
568    ) -> (Self, Option<MoveBlock>) {
569        let mut sequence = Self::new(
570            tokens,
571            max_output_tokens,
572            block_size,
573            enable_prefix_caching,
574            false,
575        );
576        let signal = sequence.take_creation_signal();
577        (sequence, signal)
578    }
579
580    /// Push a token to the sequence
581    #[cfg_attr(feature = "profile", inline(never))]
582    pub fn push(&mut self, token: u32) -> Option<Vec<MoveBlock>> {
583        match &mut self.tokens {
584            SequenceTokens::Legacy(tokens) => {
585                tokens.append(token).expect("Token push failed.");
586            }
587            SequenceTokens::Flat(tokens) => tokens.push(token),
588        }
589        self.generated_tokens += 1;
590        self.debug_assert_flat_token_invariants();
591
592        if self.len() % self.block_size != 1 {
593            return None;
594        }
595
596        // Add a partial block for the first token in a new partial sequence
597        // Send Use signal (to allocate space for this new generation block)
598        let mut signals = Vec::new();
599
600        // The scheduler may already have promoted this block at its computed
601        // boundary. Retain this fallback for callers that have not.
602        if let Some(promote) = self.promote_last_partial() {
603            signals.push(promote);
604        }
605
606        let new_partial_block = UniqueBlock::default();
607        self.unique_blocks.push(new_partial_block.clone());
608        signals.push(MoveBlock::Use(
609            vec![new_partial_block],
610            vec![],
611            vec![],
612            None,
613            None,
614        ));
615        self.debug_assert_flat_token_invariants();
616        Some(signals)
617    }
618
619    /// Generate a random token, push it to the sequence, and increment generation count.
620    ///
621    /// This function:
622    /// - Generates a random token and adds it to the current sequence
623    /// - Acquires a new partial block if needed or promotes an existing partial block to a full block
624    /// - Returns appropriate signals for the G1 manager to process
625    ///
626    /// # Panics
627    ///
628    /// Calling this function when max_output_tokens has already been reached will cause a panic.
629    /// Always check `generated_tokens < max_output_tokens` before calling this method.
630    #[cfg_attr(feature = "profile", inline(never))]
631    pub fn generate(&mut self) -> Vec<MoveBlock> {
632        self.generate_token().1
633    }
634
635    /// Generate the next output token, push it to the sequence, and return the
636    /// token alongside any KV movement signals.
637    #[cfg_attr(feature = "profile", inline(never))]
638    pub fn generate_token(&mut self) -> (u32, Vec<MoveBlock>) {
639        // Assert that we haven't reached the maximum output tokens
640        assert!(
641            self.generated_tokens < self.max_output_tokens,
642            "Cannot generate more tokens: reached max_output_tokens limit"
643        );
644
645        let token = self
646            .planned_output_ids
647            .as_ref()
648            .and_then(|ids| ids.get(self.generated_tokens).copied())
649            .unwrap_or_else(random::<u32>);
650
651        // Collect signals
652        let mut signals = Vec::new();
653
654        // Push the token to the sequence and collect any signals
655        if let Some(move_blocks) = self.push(token) {
656            signals.extend(move_blocks);
657        }
658
659        // Check if we've reached the limit after pushing
660        if self.generated_tokens != self.max_output_tokens {
661            return (token, signals);
662        }
663
664        // Free all blocks when we reach max tokens
665        signals.extend(self.terminal_signals());
666        (token, signals)
667    }
668
669    /// Release the full sequence footprint after an independent terminal
670    /// condition, such as the model context-length limit, is reached.
671    pub(crate) fn terminal_signals(&self) -> Vec<MoveBlock> {
672        self.free_signal_for_tokens(self.len())
673    }
674
675    fn free_signal_for_tokens(&self, active_tokens: usize) -> Vec<MoveBlock> {
676        let active_blocks = active_tokens
677            .div_ceil(self.block_size)
678            .min(self.unique_blocks.len());
679        if active_blocks == 0 {
680            return Vec::new();
681        }
682
683        let blocks = self.unique_blocks[..active_blocks]
684            .iter()
685            .rev()
686            .cloned()
687            .collect();
688        vec![MoveBlock::Deref(blocks)]
689    }
690
691    /// Free the currently active allocation footprint.
692    pub fn free_signal(&self) -> Vec<MoveBlock> {
693        self.free_signal_for_tokens(self.num_allocated_tokens)
694    }
695
696    /// Move the request to a preempted state and return the free signals from freeing current blocks.
697    /// Upon preemption, the sequence retains the tokens generated during the decode phase (if any).
698    /// Resets `num_allocated_tokens` so re-admission will re-allocate from scratch.
699    pub fn reset_with_signal(&mut self) -> Vec<MoveBlock> {
700        let free_signal = self.free_signal();
701        self.num_allocated_tokens = 0;
702        free_signal
703    }
704
705    /// Pops the last token in the sequence.
706    ///
707    /// This is only used to undo a freshly generated decode token after a failed
708    /// allocation/preemption path. Under that invariant, the token being removed
709    /// must be in the current partial block, so we only need to drop the trailing
710    /// partial `UniqueBlock` when the sequence length returns to an exact block
711    /// boundary. Using this to unwind arbitrary prompt history would be incorrect.
712    ///
713    /// If this contract is violated in release builds, legacy token storage
714    /// preserves its historical no-op on an empty buffer, while flat storage
715    /// panics to surface the invalid rollback.
716    pub fn pop(&mut self) {
717        debug_assert!(
718            self.generated_tokens > 0,
719            "sequence rollback requires a freshly generated token"
720        );
721        match &mut self.tokens {
722            SequenceTokens::Legacy(tokens) => {
723                tokens.pop();
724            }
725            SequenceTokens::Flat(tokens) => {
726                debug_assert!(
727                    !tokens.retained.is_empty(),
728                    "flat rollback token must be retained"
729                );
730                tokens.pop().expect("flat rollback token must be retained");
731            }
732        }
733        self.generated_tokens = self.generated_tokens.saturating_sub(1);
734
735        // Reverts to the last full block
736        if self.len().is_multiple_of(self.block_size) {
737            self.unique_blocks.pop();
738        }
739        self.debug_assert_flat_token_invariants();
740    }
741
742    fn debug_assert_flat_token_invariants(&self) {
743        if let SequenceTokens::Flat(tokens) = &self.tokens {
744            debug_assert_eq!(
745                tokens.len(),
746                self.num_input_tokens + self.generated_tokens,
747                "flat retained-token window must cover the logical sequence suffix"
748            );
749            if self.emit_token_ids {
750                debug_assert_eq!(
751                    tokens.retained_start, 0,
752                    "token-ID events require complete flat-token history"
753                );
754            } else {
755                debug_assert!(
756                    tokens.retained.len() <= self.block_size + 1,
757                    "non-emitting flat sequences retain at most one block plus the next token"
758                );
759            }
760        }
761    }
762
763    #[cfg(test)]
764    pub(crate) fn uses_flat_tokens(&self) -> bool {
765        matches!(self.tokens, SequenceTokens::Flat(_))
766    }
767
768    #[cfg(test)]
769    pub(crate) fn flat_storage_capacities(&self) -> Option<(usize, usize, usize, usize)> {
770        let SequenceTokens::Flat(tokens) = &self.tokens else {
771            return None;
772        };
773        Some((
774            tokens.retained.capacity(),
775            self.unique_blocks.capacity(),
776            self.block_hashes.capacity(),
777            self.plhs.capacity(),
778        ))
779    }
780}
781
782#[cfg(test)]
783mod tests {
784    use super::*;
785    use dynamo_tokens::SequenceHash;
786
787    fn block_hashes_from_tokens(seq: &ActiveSequence) -> Vec<BlockHash> {
788        match &seq.tokens {
789            SequenceTokens::Legacy(tokens) => tokens
790                .blocks()
791                .iter()
792                .map(|block| block.block_hash())
793                .collect(),
794            SequenceTokens::Flat(_) => seq.block_hashes().clone(),
795        }
796    }
797
798    fn assert_cached_hashes_match_promoted_blocks(seq: &ActiveSequence) {
799        let num_full_unique_blocks = seq
800            .unique_blocks()
801            .iter()
802            .filter(|block| matches!(block, UniqueBlock::FullBlock(_)))
803            .count();
804        assert_eq!(
805            seq.block_hashes().as_slice(),
806            &block_hashes_from_tokens(seq)[..num_full_unique_blocks],
807            "cached block hashes should match the promoted full blocks"
808        );
809    }
810
811    fn assert_use_signal(
812        signal: &MoveBlock,
813        expected_blocks: &[UniqueBlock],
814        expected_hashes: &[BlockHash],
815    ) {
816        match signal {
817            MoveBlock::Use(blocks, hashes, ..) => {
818                assert_eq!(blocks, expected_blocks);
819                assert_eq!(hashes, expected_hashes);
820            }
821            _ => panic!("Expected MoveBlock::Use"),
822        }
823    }
824
825    fn assert_single_partial_use(signal: &MoveBlock) {
826        match signal {
827            MoveBlock::Use(blocks, hashes, ..) => {
828                assert_eq!(blocks.len(), 1);
829                assert!(matches!(blocks[0], UniqueBlock::PartialBlock(_)));
830                assert!(hashes.is_empty());
831            }
832            _ => panic!("Expected MoveBlock::Use with a single partial block"),
833        }
834    }
835
836    fn assert_promote_parent(signal: &MoveBlock, expected_parent: Option<u64>) {
837        match signal {
838            MoveBlock::Promote(_, _, parent_hash, _hash, ..) => {
839                assert_eq!(*parent_hash, expected_parent);
840            }
841            _ => panic!("Expected MoveBlock::Promote"),
842        }
843    }
844
845    fn assert_deref_blocks(signal: &MoveBlock, expected: &[UniqueBlock]) {
846        match signal {
847            MoveBlock::Deref(blocks) => {
848                assert_eq!(blocks, expected);
849            }
850            _ => panic!("Expected MoveBlock::Deref"),
851        }
852    }
853
854    #[test]
855    fn test_new_with_signal_creates_initial_partial_block() {
856        let initial_tokens: Vec<u32> = (0..15).collect();
857        let (seq, signal) = ActiveSequence::new_with_signal(initial_tokens, 100, Some(16), true);
858
859        assert_eq!(seq.num_input_tokens(), 15);
860        assert_eq!(seq.len(), 15);
861        assert_single_partial_use(signal.as_ref().expect("Expected initial Use signal"));
862    }
863
864    #[test]
865    fn test_push_across_block_boundary_promotes_and_allocates_partial() {
866        let initial_tokens: Vec<u32> = (0..15).collect();
867        let (mut seq, _) = ActiveSequence::new_with_signal(initial_tokens, 100, Some(16), true);
868
869        let signal_15 = seq.push(15);
870        assert!(
871            signal_15.is_none(),
872            "Completing a block should not trigger signals"
873        );
874
875        let signal_16 = seq.push(16).expect("Expected boundary crossing signals");
876        assert_eq!(signal_16.len(), 2);
877        assert_promote_parent(&signal_16[0], None);
878        assert_single_partial_use(&signal_16[1]);
879
880        assert_eq!(
881            seq.unique_blocks().len(),
882            2,
883            "sequence should have one full block and one partial block"
884        );
885        assert_eq!(
886            seq.len() % seq.block_size(),
887            1,
888            "sequence should have one token in the new partial block"
889        );
890    }
891
892    #[test]
893    fn test_equivalent_histories_preserve_full_block_identity() {
894        let initial_tokens: Vec<u32> = (0..15).collect();
895        let (mut seq1, _) = ActiveSequence::new_with_signal(initial_tokens, 100, Some(16), true);
896        seq1.push(15);
897        seq1.push(16);
898
899        let extended_tokens: Vec<u32> = (0..16).collect();
900        let (mut seq2, _) = ActiveSequence::new_with_signal(extended_tokens, 100, Some(16), true);
901        seq2.push(16);
902        seq2.pop();
903        seq2.push(16);
904
905        assert_eq!(seq1.unique_blocks()[0], seq2.unique_blocks()[0]);
906        assert_ne!(seq1.unique_blocks()[1], seq2.unique_blocks()[1]);
907    }
908
909    #[test]
910    fn test_promote_uses_previous_full_block_as_parent() {
911        let initial_tokens: Vec<u32> = (0..15).collect();
912        let (mut seq, _) = ActiveSequence::new_with_signal(initial_tokens, 100, Some(16), true);
913        seq.push(15);
914        seq.push(16);
915
916        seq.push(17);
917        seq.pop();
918        seq.pop();
919        seq.push(16);
920
921        let extended_tokens: Vec<u32> = (0..16).collect();
922        let (mut seq_equiv, _) =
923            ActiveSequence::new_with_signal(extended_tokens, 100, Some(16), true);
924        seq_equiv.push(16);
925        seq_equiv.pop();
926        seq_equiv.push(16);
927        for token in 17..33 {
928            seq.push(token);
929            seq_equiv.push(token);
930        }
931
932        assert_eq!(
933            &seq.unique_blocks()[0..2],
934            &seq_equiv.unique_blocks()[0..2],
935            "first two full blocks should remain identical"
936        );
937
938        for token in 33..48 {
939            seq.push(token);
940        }
941
942        let signal = seq
943            .push(48)
944            .expect("Expected promote when opening next partial");
945
946        let UniqueBlock::FullBlock(expected_hash) = seq.unique_blocks()[1] else {
947            panic!("unique_blocks[1] should be a full block");
948        };
949        assert_promote_parent(&signal[0], Some(expected_hash));
950        assert_single_partial_use(&signal[1]);
951    }
952
953    #[test]
954    fn test_reset_with_signal_frees_blocks_and_resets_allocation() {
955        let initial_tokens: Vec<u32> = (0..15).collect();
956        let (mut seq, _) = ActiveSequence::new_with_signal(initial_tokens, 100, Some(16), true);
957        seq.push(15);
958        seq.push(16);
959        seq.commit_allocation(seq.len());
960
961        let free_signals = seq.reset_with_signal();
962
963        assert_eq!(free_signals.len(), 1);
964        let expected = seq
965            .unique_blocks()
966            .iter()
967            .rev()
968            .cloned()
969            .collect::<Vec<_>>();
970        assert_deref_blocks(&free_signals[0], &expected);
971        assert_eq!(seq.num_allocated_tokens(), 0);
972        assert_eq!(seq.generated_tokens(), 2);
973    }
974
975    #[test]
976    fn test_free_signal_is_empty_without_an_active_allocation() {
977        let seq = ActiveSequence::new((0..10).collect(), 4, Some(4), true, false);
978
979        assert!(seq.free_signal().is_empty());
980    }
981
982    #[test]
983    fn test_free_signal_batches_allocated_blocks_in_reverse_order() {
984        let mut seq = ActiveSequence::new((0..10).collect(), 4, Some(4), true, false);
985        seq.commit_allocation(seq.len());
986
987        let expected = seq
988            .unique_blocks()
989            .iter()
990            .rev()
991            .cloned()
992            .collect::<Vec<_>>();
993        let signals = seq.free_signal();
994
995        assert_eq!(signals.len(), 1);
996        assert_deref_blocks(&signals[0], &expected);
997    }
998
999    #[test]
1000    fn test_active_sequence_generate_signals() {
1001        // Create a sequence with block size 16, max_output_tokens 4, initialized with tokens [0..14)
1002        let initial_tokens: Vec<u32> = (0..14).collect();
1003        let (mut seq, signal) = ActiveSequence::new_with_signal(initial_tokens, 5, Some(16), true);
1004
1005        // Initial signal - should have received a Use signal for the partial block
1006        assert_single_partial_use(signal.as_ref().expect("Expected initial Use signal"));
1007
1008        // Generate first two tokens - should not trigger new signals
1009        seq.generate();
1010        let signals_first = seq.generate();
1011        assert_eq!(signals_first.len(), 0);
1012
1013        // Generate third token - this fills the block and should trigger both Promote and Use signals
1014        let signals_second = seq.generate();
1015        assert_eq!(signals_second.len(), 2);
1016
1017        // First signal should be Promote
1018        assert_promote_parent(&signals_second[0], None);
1019
1020        // Second signal should be Use for new partial block
1021        assert_single_partial_use(&signals_second[1]);
1022
1023        // Generate fourth token - should not trigger new signals as it's adding to partial block
1024        let signals_third = seq.generate();
1025        assert_eq!(signals_third.len(), 0);
1026
1027        // Generate last token - we reach max_output_tokens, so all blocks should
1028        // be dereferenced in one reverse-ordered batch.
1029        let expected = seq
1030            .unique_blocks()
1031            .iter()
1032            .rev()
1033            .cloned()
1034            .collect::<Vec<_>>();
1035        let signals_last = seq.generate();
1036        assert_eq!(signals_last.len(), 1);
1037        assert_deref_blocks(&signals_last[0], &expected);
1038    }
1039
1040    #[test]
1041    fn test_prepare_allocation_slices_full_and_partial_blocks() {
1042        let tokens: Vec<u32> = (0..10).collect();
1043        let seq = ActiveSequence::new(tokens, 4, Some(4), true, false);
1044
1045        let first = seq.prepare_allocation(4).unwrap();
1046        assert_use_signal(
1047            &first,
1048            &seq.unique_blocks()[0..1],
1049            &seq.block_hashes()[0..1],
1050        );
1051
1052        let second = seq.prepare_allocation(8).unwrap();
1053        assert_use_signal(
1054            &second,
1055            &seq.unique_blocks()[0..2],
1056            &seq.block_hashes()[0..2],
1057        );
1058
1059        let third = seq.prepare_allocation(10).unwrap();
1060        assert_use_signal(
1061            &third,
1062            &seq.unique_blocks()[0..3],
1063            &seq.block_hashes()[0..2],
1064        );
1065    }
1066
1067    #[test]
1068    fn test_prepare_allocation_is_stable_until_commit() {
1069        let tokens: Vec<u32> = (0..10).collect();
1070        let mut seq = ActiveSequence::new(tokens, 4, Some(4), true, false);
1071
1072        let first = seq.prepare_allocation(4).unwrap();
1073        let second = seq.prepare_allocation(4).unwrap();
1074        assert_eq!(first, second);
1075
1076        seq.commit_allocation(4);
1077        let next = seq.prepare_allocation(8).unwrap();
1078        assert_use_signal(&next, &seq.unique_blocks()[1..2], &seq.block_hashes()[1..2]);
1079    }
1080
1081    #[test]
1082    fn test_block_hash_cache_stays_in_sync_after_promote_and_pop() {
1083        let initial_tokens: Vec<u32> = (0..15).collect();
1084        let (mut seq, _) = ActiveSequence::new_with_signal(initial_tokens, 4, Some(16), true);
1085
1086        assert_cached_hashes_match_promoted_blocks(&seq);
1087
1088        seq.push(15);
1089        assert_cached_hashes_match_promoted_blocks(&seq);
1090
1091        let promote_signals = seq.push(16).unwrap();
1092        assert_eq!(promote_signals.len(), 2);
1093        assert_cached_hashes_match_promoted_blocks(&seq);
1094
1095        // `pop()` is only valid for undoing a freshly generated token from the
1096        // current partial block; this is the replay/preemption path we rely on.
1097        seq.pop();
1098        assert_cached_hashes_match_promoted_blocks(&seq);
1099    }
1100
1101    #[derive(Debug, PartialEq)]
1102    enum SignalShape {
1103        Use {
1104            blocks: Vec<Option<u64>>,
1105            hashes: Vec<BlockHash>,
1106            plhs: Vec<PositionalLineageHash>,
1107            token_ids: Option<Vec<Vec<u32>>>,
1108            parent: Option<Option<u64>>,
1109        },
1110        Deref(Vec<Option<u64>>),
1111        Promote {
1112            sequence_hash: SequenceHash,
1113            parent_hash: Option<u64>,
1114            block_hash: Option<BlockHash>,
1115            plh: PositionalLineageHash,
1116            token_ids: Option<Vec<u32>>,
1117        },
1118    }
1119
1120    fn block_shape(block: &UniqueBlock) -> Option<u64> {
1121        match block {
1122            UniqueBlock::FullBlock(hash) => Some(*hash),
1123            UniqueBlock::PartialBlock(_) => None,
1124        }
1125    }
1126
1127    fn signal_shape(signal: MoveBlock) -> SignalShape {
1128        match signal {
1129            MoveBlock::Use(blocks, hashes, plhs, token_ids, parent) => SignalShape::Use {
1130                blocks: blocks.iter().map(block_shape).collect(),
1131                hashes,
1132                plhs,
1133                token_ids,
1134                parent: parent.as_ref().map(block_shape),
1135            },
1136            MoveBlock::Deref(blocks) => {
1137                SignalShape::Deref(blocks.iter().map(block_shape).collect())
1138            }
1139            MoveBlock::Promote(_, sequence_hash, parent_hash, block_hash, plh, token_ids) => {
1140                SignalShape::Promote {
1141                    sequence_hash,
1142                    parent_hash,
1143                    block_hash,
1144                    plh,
1145                    token_ids,
1146                }
1147            }
1148        }
1149    }
1150
1151    fn signal_shapes(signals: impl IntoIterator<Item = MoveBlock>) -> Vec<SignalShape> {
1152        signals.into_iter().map(signal_shape).collect()
1153    }
1154
1155    fn sequence_pair(
1156        prompt: Vec<u32>,
1157        output_ids: Vec<u32>,
1158        block_size: usize,
1159        emit_token_ids: bool,
1160    ) -> (ActiveSequence, ActiveSequence) {
1161        let legacy = ActiveSequence::new_with_planned_output_ids(
1162            prompt.clone(),
1163            output_ids.len(),
1164            Some(block_size),
1165            true,
1166            emit_token_ids,
1167            Some(output_ids.clone()),
1168        );
1169        let flat = ActiveSequence::new_flat_with_planned_output_ids(
1170            prompt,
1171            output_ids.len(),
1172            output_ids.len(),
1173            block_size,
1174            true,
1175            emit_token_ids,
1176            Some(output_ids),
1177        );
1178        assert!(!legacy.uses_flat_tokens());
1179        assert!(flat.uses_flat_tokens());
1180        (legacy, flat)
1181    }
1182
1183    fn assert_sequence_parity(legacy: &ActiveSequence, flat: &ActiveSequence) {
1184        assert_eq!(legacy.len(), flat.len());
1185        assert_eq!(legacy.extra_tokens(), flat.extra_tokens());
1186        assert_eq!(legacy.emit_token_ids(), flat.emit_token_ids());
1187        assert_eq!(legacy.block_hashes(), flat.block_hashes());
1188        assert_eq!(
1189            legacy.positional_lineage_hashes(),
1190            flat.positional_lineage_hashes()
1191        );
1192        assert_eq!(
1193            legacy
1194                .unique_blocks()
1195                .iter()
1196                .map(block_shape)
1197                .collect::<Vec<_>>(),
1198            flat.unique_blocks()
1199                .iter()
1200                .map(block_shape)
1201                .collect::<Vec<_>>()
1202        );
1203        assert_eq!(legacy.generated_tokens(), flat.generated_tokens());
1204        assert_eq!(legacy.num_input_tokens(), flat.num_input_tokens());
1205        assert_eq!(legacy.num_allocated_tokens(), flat.num_allocated_tokens());
1206        if flat.emit_token_ids() {
1207            assert_eq!(legacy.block_token_ids(), flat.block_token_ids());
1208        } else {
1209            let SequenceTokens::Flat(tokens) = &flat.tokens else {
1210                panic!("expected flat token storage");
1211            };
1212            assert!(
1213                tokens.retained.len() <= flat.block_size() + 1,
1214                "non-emitting flat storage exceeded one block plus one token"
1215            );
1216        }
1217    }
1218
1219    #[test]
1220    fn flat_sequence_matches_legacy_across_prompt_and_output_boundaries() {
1221        const BLOCK_SIZE: usize = 16;
1222        for promote_eagerly in [true, false] {
1223            for emit_token_ids in [false, true] {
1224                for prompt_len in [0, 1, BLOCK_SIZE - 1, BLOCK_SIZE, BLOCK_SIZE + 1, 53] {
1225                    let prompt: Vec<u32> = (0..prompt_len as u32).collect();
1226                    let outputs: Vec<u32> =
1227                        (10_000..10_000 + (BLOCK_SIZE * 2 + 3) as u32).collect();
1228                    let (mut legacy, mut flat) =
1229                        sequence_pair(prompt, outputs.clone(), BLOCK_SIZE, emit_token_ids);
1230                    let SequenceTokens::Flat(tokens) = &flat.tokens else {
1231                        panic!("expected flat token storage");
1232                    };
1233                    let token_capacity = tokens.retained.capacity();
1234                    let mut push_promotions = 0;
1235
1236                    assert_sequence_parity(&legacy, &flat);
1237                    assert_eq!(
1238                        legacy.take_creation_signal().map(signal_shape),
1239                        flat.take_creation_signal().map(signal_shape)
1240                    );
1241
1242                    for expected_token in outputs {
1243                        let (legacy_token, legacy_signals) = legacy.generate_token();
1244                        let (flat_token, flat_signals) = flat.generate_token();
1245                        assert_eq!(legacy_token, expected_token);
1246                        assert_eq!(flat_token, expected_token);
1247
1248                        let legacy_push_promoted = legacy_signals
1249                            .iter()
1250                            .any(|signal| matches!(signal, MoveBlock::Promote(..)));
1251                        let flat_push_promoted = flat_signals
1252                            .iter()
1253                            .any(|signal| matches!(signal, MoveBlock::Promote(..)));
1254                        assert_eq!(legacy_push_promoted, flat_push_promoted);
1255                        if flat_push_promoted {
1256                            push_promotions += 1;
1257                        }
1258                        assert_eq!(signal_shapes(legacy_signals), signal_shapes(flat_signals));
1259                        assert_sequence_parity(&legacy, &flat);
1260
1261                        let SequenceTokens::Flat(tokens) = &flat.tokens else {
1262                            panic!("expected flat token storage");
1263                        };
1264                        assert_eq!(tokens.retained.capacity(), token_capacity);
1265                        if flat_push_promoted && !emit_token_ids {
1266                            assert_eq!(tokens.retained.len(), 1);
1267                            assert_eq!(tokens.retained_start + 1, flat.len());
1268                        }
1269
1270                        if promote_eagerly
1271                            && legacy.generated_tokens() < legacy.max_output_tokens()
1272                            && legacy.len().is_multiple_of(BLOCK_SIZE)
1273                        {
1274                            assert_eq!(
1275                                legacy.promote_computed_tail(legacy.len()).map(signal_shape),
1276                                flat.promote_computed_tail(flat.len()).map(signal_shape)
1277                            );
1278                            assert_sequence_parity(&legacy, &flat);
1279                        }
1280                    }
1281
1282                    if promote_eagerly {
1283                        assert_eq!(push_promotions, 0);
1284                    } else {
1285                        assert!(push_promotions > 0);
1286                    }
1287                    assert_eq!(
1288                        signal_shapes(legacy.terminal_signals()),
1289                        signal_shapes(flat.terminal_signals())
1290                    );
1291                }
1292            }
1293        }
1294    }
1295
1296    #[test]
1297    fn flat_sequence_matches_chunked_token_id_allocations() {
1298        const BLOCK_SIZE: usize = 4;
1299        let prompt: Vec<u32> = (0..12).collect();
1300        let (mut legacy, mut flat) = sequence_pair(prompt.clone(), Vec::new(), BLOCK_SIZE, true);
1301
1302        for cumulative_tokens in [4, 8, 12] {
1303            let legacy_signal = legacy
1304                .prepare_allocation(cumulative_tokens)
1305                .expect("legacy chunk should allocate");
1306            let flat_signal = flat
1307                .prepare_allocation(cumulative_tokens)
1308                .expect("flat chunk should allocate");
1309            assert_eq!(
1310                signal_shape(legacy_signal),
1311                signal_shape(flat_signal.clone())
1312            );
1313            let MoveBlock::Use(_, _, _, Some(token_ids), _) = flat_signal else {
1314                panic!("chunked native allocation must include token IDs");
1315            };
1316            let start = cumulative_tokens - BLOCK_SIZE;
1317            assert_eq!(token_ids, vec![prompt[start..cumulative_tokens].to_vec()]);
1318            legacy.commit_allocation(cumulative_tokens);
1319            flat.commit_allocation(cumulative_tokens);
1320        }
1321    }
1322
1323    #[test]
1324    fn flat_sequence_capacities_do_not_grow_during_decode() {
1325        const BLOCK_SIZE: usize = 16;
1326        let prompt: Vec<u32> = (0..17).collect();
1327        let outputs: Vec<u32> = (1_000..1_037).collect();
1328
1329        for emit_token_ids in [false, true] {
1330            let mut flat = ActiveSequence::new_flat_with_planned_output_ids(
1331                prompt.clone(),
1332                outputs.len(),
1333                outputs.len(),
1334                BLOCK_SIZE,
1335                true,
1336                emit_token_ids,
1337                Some(outputs.clone()),
1338            );
1339            let metadata_capacities = (
1340                flat.unique_blocks.capacity(),
1341                flat.block_hashes.capacity(),
1342                flat.plhs.capacity(),
1343            );
1344            let SequenceTokens::Flat(tokens) = &flat.tokens else {
1345                panic!("expected flat token storage");
1346            };
1347            let token_capacity = tokens.retained.capacity();
1348            if emit_token_ids {
1349                assert!(token_capacity >= prompt.len() + outputs.len());
1350            } else {
1351                assert!(token_capacity > BLOCK_SIZE);
1352                assert_eq!(tokens.retained_start, BLOCK_SIZE);
1353                assert_eq!(tokens.retained, prompt[BLOCK_SIZE..]);
1354            }
1355
1356            for _ in &outputs {
1357                flat.generate_token();
1358                if flat.generated_tokens() < flat.max_output_tokens()
1359                    && flat.len().is_multiple_of(BLOCK_SIZE)
1360                {
1361                    flat.promote_computed_tail(flat.len());
1362                }
1363
1364                assert_eq!(
1365                    metadata_capacities,
1366                    (
1367                        flat.unique_blocks.capacity(),
1368                        flat.block_hashes.capacity(),
1369                        flat.plhs.capacity(),
1370                    )
1371                );
1372                let SequenceTokens::Flat(tokens) = &flat.tokens else {
1373                    panic!("expected flat token storage");
1374                };
1375                assert_eq!(tokens.retained.capacity(), token_capacity);
1376                if emit_token_ids {
1377                    assert_eq!(tokens.retained_start, 0);
1378                } else {
1379                    assert!(tokens.retained.len() <= BLOCK_SIZE + 1);
1380                }
1381            }
1382        }
1383    }
1384
1385    fn assert_uncached_signal_parity(left: Vec<MoveBlock>, right: Vec<MoveBlock>) {
1386        assert_eq!(left.len(), right.len());
1387        for (left, right) in left.into_iter().zip(right) {
1388            match (left, right) {
1389                (
1390                    MoveBlock::Use(lb, lh, lp, lt, lparent),
1391                    MoveBlock::Use(rb, rh, rp, rt, rparent),
1392                ) => {
1393                    assert_eq!(
1394                        lb.iter()
1395                            .map(block_shape)
1396                            .map(|hash| hash.is_some())
1397                            .collect::<Vec<_>>(),
1398                        rb.iter()
1399                            .map(block_shape)
1400                            .map(|hash| hash.is_some())
1401                            .collect::<Vec<_>>()
1402                    );
1403                    assert_eq!(lh.len(), rh.len());
1404                    assert_eq!(lp.len(), rp.len());
1405                    assert_eq!(lt.is_some(), rt.is_some());
1406                    assert_eq!(lparent.is_some(), rparent.is_some());
1407                }
1408                (
1409                    MoveBlock::Promote(_, _, lp, lbh, lplh, lt),
1410                    MoveBlock::Promote(_, _, rp, rbh, rplh, rt),
1411                ) => {
1412                    assert_eq!(lp.is_some(), rp.is_some());
1413                    assert_eq!(lbh.is_some(), rbh.is_some());
1414                    assert_eq!(lplh.position(), rplh.position());
1415                    assert_eq!(lt.is_some(), rt.is_some());
1416                }
1417                (MoveBlock::Deref(lb), MoveBlock::Deref(rb)) => {
1418                    assert_eq!(lb.len(), rb.len());
1419                    assert_eq!(
1420                        lb.iter()
1421                            .map(block_shape)
1422                            .map(|hash| hash.is_some())
1423                            .collect::<Vec<_>>(),
1424                        rb.iter()
1425                            .map(block_shape)
1426                            .map(|hash| hash.is_some())
1427                            .collect::<Vec<_>>()
1428                    );
1429                }
1430                (left, right) => panic!("signal variants differ: {left:?} != {right:?}"),
1431            }
1432        }
1433    }
1434
1435    #[test]
1436    fn flat_sequence_matches_uncached_legacy_structure() {
1437        const BLOCK_SIZE: usize = 4;
1438        let prompt: Vec<u32> = (0..9).collect();
1439        let outputs: Vec<u32> = (100..108).collect();
1440        let mut legacy = ActiveSequence::new_with_planned_output_ids(
1441            prompt.clone(),
1442            outputs.len(),
1443            Some(BLOCK_SIZE),
1444            false,
1445            true,
1446            Some(outputs.clone()),
1447        );
1448        let mut flat = ActiveSequence::new_flat_with_planned_output_ids(
1449            prompt,
1450            outputs.len(),
1451            outputs.len(),
1452            BLOCK_SIZE,
1453            false,
1454            true,
1455            Some(outputs),
1456        );
1457
1458        assert!(!legacy.emit_token_ids());
1459        assert!(!flat.emit_token_ids());
1460        assert!(legacy.block_hashes().is_empty());
1461        assert!(flat.block_hashes().is_empty());
1462        assert_eq!(legacy.unique_blocks().len(), flat.unique_blocks().len());
1463        assert_eq!(
1464            legacy
1465                .positional_lineage_hashes()
1466                .iter()
1467                .map(PositionalLineageHash::position)
1468                .collect::<Vec<_>>(),
1469            flat.positional_lineage_hashes()
1470                .iter()
1471                .map(PositionalLineageHash::position)
1472                .collect::<Vec<_>>()
1473        );
1474        assert_uncached_signal_parity(
1475            legacy.take_creation_signal().into_iter().collect(),
1476            flat.take_creation_signal().into_iter().collect(),
1477        );
1478
1479        while legacy.generated_tokens() < legacy.max_output_tokens() {
1480            let (_, legacy_signals) = legacy.generate_token();
1481            let (_, flat_signals) = flat.generate_token();
1482            assert_uncached_signal_parity(legacy_signals, flat_signals);
1483        }
1484        assert_uncached_signal_parity(legacy.terminal_signals(), flat.terminal_signals());
1485    }
1486
1487    #[test]
1488    #[should_panic(expected = "partial block cannot be a parent")]
1489    fn flat_promotion_rejects_partial_parent() {
1490        let mut flat = ActiveSequence::new_flat_with_planned_output_ids(
1491            (0..15).collect(),
1492            1,
1493            1,
1494            16,
1495            true,
1496            false,
1497            Some(vec![99]),
1498        );
1499        flat.push(99);
1500        flat.unique_blocks.insert(0, UniqueBlock::default());
1501        flat.promote_computed_tail(flat.len());
1502    }
1503
1504    #[test]
1505    #[should_panic(expected = "flat sequences retain full token history")]
1506    fn non_emitting_flat_sequence_rejects_token_materialization() {
1507        let flat = ActiveSequence::new_flat_with_planned_output_ids(
1508            (0..16).collect(),
1509            1,
1510            1,
1511            16,
1512            true,
1513            false,
1514            None,
1515        );
1516        flat.block_token_ids();
1517    }
1518
1519    #[test]
1520    fn flat_sequence_matches_legacy_reset_and_one_token_rollback() {
1521        const BLOCK_SIZE: usize = 16;
1522        let prompt: Vec<u32> = (0..BLOCK_SIZE as u32).collect();
1523        let (mut legacy, mut flat) = sequence_pair(prompt, vec![1_001, 1_002], BLOCK_SIZE, false);
1524
1525        legacy.take_creation_signal();
1526        flat.take_creation_signal();
1527        assert_eq!(
1528            signal_shapes(legacy.push(1_001).unwrap()),
1529            signal_shapes(flat.push(1_001).unwrap())
1530        );
1531        legacy.pop();
1532        flat.pop();
1533        assert_sequence_parity(&legacy, &flat);
1534
1535        legacy.commit_allocation(legacy.len());
1536        flat.commit_allocation(flat.len());
1537        assert_eq!(
1538            signal_shapes(legacy.reset_with_signal()),
1539            signal_shapes(flat.reset_with_signal())
1540        );
1541    }
1542
1543    #[test]
1544    fn flat_sequence_preserves_uncached_random_identity_behavior() {
1545        let make = || {
1546            ActiveSequence::new_flat_with_planned_output_ids(
1547                (0..17).collect(),
1548                16,
1549                16,
1550                16,
1551                false,
1552                true,
1553                None,
1554            )
1555        };
1556        let mut first = make();
1557        let second = make();
1558
1559        assert!(first.block_hashes().is_empty());
1560        assert_ne!(first.unique_blocks()[0], second.unique_blocks()[0]);
1561        assert_ne!(
1562            first.positional_lineage_hashes()[0],
1563            second.positional_lineage_hashes()[0]
1564        );
1565
1566        for token in 0..15 {
1567            first.push(token);
1568        }
1569        let promote = first
1570            .promote_computed_tail(first.len())
1571            .expect("completed uncached block should promote");
1572        let MoveBlock::Promote(_, _, _, block_hash, plh, token_ids) = promote else {
1573            panic!("expected promote signal");
1574        };
1575        assert!(block_hash.is_none());
1576        assert_eq!(plh.parent_hash_fragment(), 0);
1577        assert!(token_ids.is_none());
1578    }
1579}