use std::collections::VecDeque;
use super::{NativeMtpDraft, NativeMtpDraftOrigin, NativeMtpHybridProposal};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(in crate::frontend) struct PipelinedCandidateChunk {
proposal_tokens: Vec<i32>,
native_mtp_token_count: usize,
starts_epoch: bool,
}
impl PipelinedCandidateChunk {
pub(in crate::frontend) fn proposal_tokens(&self) -> &[i32] {
&self.proposal_tokens
}
pub(in crate::frontend) fn native_mtp_token_count(&self) -> usize {
self.native_mtp_token_count
}
pub(in crate::frontend) fn starts_epoch(&self) -> bool {
self.starts_epoch
}
pub(in crate::frontend) fn advance_tokens(&self) -> usize {
self.proposal_tokens.len()
}
}
pub(in crate::frontend) fn pipelined_target_commit_count(
planned_advance_tokens: usize,
target_commit_count: usize,
fully_accepted: bool,
dependent_work_exists: bool,
) -> usize {
if fully_accepted && dependent_work_exists {
planned_advance_tokens
} else {
target_commit_count
}
}
#[derive(Debug)]
pub(in crate::frontend) struct CompositeProposalPipeline {
proposal: NativeMtpHybridProposal,
origin: Option<NativeMtpDraftOrigin>,
candidates: VecDeque<i32>,
dispatched_tokens: usize,
dispatched_native_mtp_token_count: usize,
accepted_tokens: usize,
next_draft: Option<NativeMtpDraft>,
}
impl CompositeProposalPipeline {
pub(in crate::frontend) fn new(
proposal: NativeMtpHybridProposal,
origin: Option<NativeMtpDraftOrigin>,
) -> Self {
Self {
candidates: proposal.tokens().iter().copied().collect(),
proposal,
origin,
dispatched_tokens: 0,
dispatched_native_mtp_token_count: 0,
accepted_tokens: 0,
next_draft: None,
}
}
pub(in crate::frontend) fn next_chunk(
&mut self,
max_tokens: usize,
) -> Option<PipelinedCandidateChunk> {
let chunk_len = max_tokens.max(1).min(self.candidates.len());
if chunk_len == 0 {
return None;
}
let starts_epoch = self.dispatched_tokens == 0;
let proposal_tokens = self.candidates.drain(..chunk_len).collect::<Vec<_>>();
let native_mtp_token_count = self
.proposal
.native_mtp_token_count()
.saturating_sub(self.dispatched_native_mtp_token_count)
.min(proposal_tokens.len());
self.dispatched_native_mtp_token_count += native_mtp_token_count;
self.dispatched_tokens += proposal_tokens.len();
Some(PipelinedCandidateChunk {
proposal_tokens,
native_mtp_token_count,
starts_epoch,
})
}
pub(in crate::frontend) fn proposal(&self) -> &NativeMtpHybridProposal {
&self.proposal
}
pub(in crate::frontend) fn origin(&self) -> Option<NativeMtpDraftOrigin> {
self.origin
}
pub(in crate::frontend) fn has_remaining_candidates(&self) -> bool {
!self.candidates.is_empty()
}
pub(in crate::frontend) fn candidate_len(&self) -> usize {
self.candidates.len()
}
pub(in crate::frontend) fn optimistic_suffix(&self) -> &[i32] {
&self.proposal.tokens()[self.accepted_tokens.min(self.proposal.tokens().len())..]
}
pub(in crate::frontend) fn append_ngram_candidates(&mut self, tokens: &[i32]) -> usize {
self.proposal.append_ngram_tokens(tokens);
self.candidates.extend(tokens.iter().copied());
tokens.len()
}
pub(in crate::frontend) fn observe_accepted(&mut self, count: usize) {
self.accepted_tokens += count;
}
pub(in crate::frontend) fn accepted_tokens(&self) -> usize {
self.accepted_tokens
}
pub(in crate::frontend) fn set_next_draft(
&mut self,
native_mtp_enabled: bool,
draft: Option<NativeMtpDraft>,
) {
self.next_draft = native_mtp_enabled.then_some(draft).flatten();
}
pub(in crate::frontend) fn next_draft(&self) -> Option<&NativeMtpDraft> {
self.next_draft.as_ref()
}
pub(in crate::frontend) fn take_next_draft(&mut self) -> Option<NativeMtpDraft> {
self.next_draft.take()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn proposal(tokens: Vec<i32>, native_mtp_tokens: usize) -> NativeMtpHybridProposal {
let ngram_span_available = native_mtp_tokens < tokens.len();
NativeMtpHybridProposal::from_parts(tokens, native_mtp_tokens, ngram_span_available)
}
#[test]
fn marks_only_the_first_full_chunk_as_epoch_start() {
let mut pipeline = CompositeProposalPipeline::new(
proposal(vec![9, 1, 2, 3, 4], 1),
Some(NativeMtpDraftOrigin::InitialSerial),
);
let first = pipeline.next_chunk(3).unwrap();
assert_eq!(first.proposal_tokens(), [9, 1, 2]);
assert_eq!(first.native_mtp_token_count(), 1);
assert_eq!(first.advance_tokens(), 3);
assert!(first.starts_epoch());
let second = pipeline.next_chunk(3).unwrap();
assert_eq!(second.proposal_tokens(), [3, 4]);
assert_eq!(second.native_mtp_token_count(), 0);
assert!(!second.starts_epoch());
}
#[test]
fn dependent_chunk_keeps_the_free_target_at_the_boundary() {
let mut pipeline = CompositeProposalPipeline::new(proposal(vec![9, 1, 2, 3], 1), None);
let first = pipeline.next_chunk(2).unwrap();
assert_eq!(
pipelined_target_commit_count(first.advance_tokens(), 3, true, true),
2
);
assert_eq!(pipeline.next_chunk(2).unwrap().proposal_tokens(), [2, 3]);
}
#[test]
fn terminal_chunk_commits_the_free_target() {
let mut pipeline = CompositeProposalPipeline::new(proposal(vec![9, 1], 1), None);
let chunk = pipeline.next_chunk(2).unwrap();
assert_eq!(
pipelined_target_commit_count(chunk.advance_tokens(), 3, true, false),
3
);
}
#[test]
fn supports_a_pure_ngram_candidate() {
let mut pipeline = CompositeProposalPipeline::new(proposal(vec![1, 2, 3], 0), None);
let chunk = pipeline.next_chunk(2).unwrap();
assert_eq!(chunk.proposal_tokens(), [1, 2]);
assert_eq!(chunk.native_mtp_token_count(), 0);
assert!(pipeline.has_remaining_candidates());
}
#[test]
fn pure_ngram_pipeline_discards_verify_next_native_mtp_drafts() {
let mut pipeline = CompositeProposalPipeline::new(proposal(vec![1, 2, 3], 0), None);
pipeline.set_next_draft(
false,
Some(NativeMtpDraft {
tokens: vec![4],
proposal_compute_us: 12,
}),
);
assert!(pipeline.next_draft().is_none());
}
#[test]
fn records_the_matching_prefix_of_a_rejected_window() {
let mut pipeline = CompositeProposalPipeline::new(
proposal(vec![9, 1, 2, 3], 1),
Some(NativeMtpDraftOrigin::InitialSerial),
);
let _ = pipeline.next_chunk(1).unwrap();
pipeline.observe_accepted(1);
assert_eq!(pipeline.accepted_tokens(), 1);
assert!(
pipeline
.proposal()
.ngram_tail_rejected(pipeline.accepted_tokens())
);
}
#[test]
fn later_ngram_rejection_does_not_reject_an_accepted_native_prefix() {
let mut pipeline = CompositeProposalPipeline::new(
proposal(vec![9, 1, 2, 3], 1),
Some(NativeMtpDraftOrigin::InitialSerial),
);
let first = pipeline.next_chunk(1).unwrap();
assert_eq!(first.proposal_tokens(), [9]);
pipeline.observe_accepted(1);
let second = pipeline.next_chunk(1).unwrap();
assert_eq!(second.proposal_tokens(), [1]);
pipeline.observe_accepted(0);
assert!(
!pipeline
.proposal()
.native_mtp_prefix_rejected(pipeline.accepted_tokens())
);
assert!(
pipeline
.proposal()
.ngram_tail_rejected(pipeline.accepted_tokens())
);
}
#[test]
fn appends_an_optimistic_ngram_suffix_without_committing_it() {
let mut pipeline = CompositeProposalPipeline::new(
proposal(vec![9, 1, 2, 3], 1),
Some(NativeMtpDraftOrigin::InitialSerial),
);
let first = pipeline.next_chunk(1).unwrap();
assert_eq!(first.proposal_tokens(), [9]);
assert_eq!(pipeline.optimistic_suffix(), &[9, 1, 2, 3]);
pipeline.observe_accepted(1);
assert_eq!(pipeline.optimistic_suffix(), &[1, 2, 3]);
assert_eq!(pipeline.append_ngram_candidates(&[4, 5]), 2);
assert_eq!(pipeline.optimistic_suffix(), &[1, 2, 3, 4, 5]);
assert_eq!(pipeline.proposal().tokens(), &[9, 1, 2, 3, 4, 5]);
assert_eq!(pipeline.proposal().ngram_token_count(), 5);
let second = pipeline.next_chunk(1).unwrap();
assert_eq!(second.proposal_tokens(), [1]);
}
}