feat(batch9): token merging (ToMe), grad accum per-step norm, speculative streaming
CI / Format Check (push) Failing after 11s
CI / Build (ubuntu-latest) (push) Failing after 32s
CI / Build CPU-Only (Explicit) (push) Failing after 37s
CI / Clippy Check (push) Failing after 38s
GPU Tests / Check GPU Availability (push) Successful in 0s
GPU Tests / CUDA Tests (11.8) (push) Has been skipped
GPU Tests / CUDA Tests (12.1) (push) Has been skipped
Documentation / Build API Documentation (push) Failing after 24s
Performance Benchmarks / Run Benchmarks (push) Successful in 1m12s
Documentation / Build User Guide (push) Failing after 34s
CI / Build (macos-latest) (push) Failing after 43s
CI / Test (macos-latest) (push) Has been skipped
CI / Test (ubuntu-latest) (push) Has been skipped
CI / Python Bindings (maturin) (macos-latest) (push) Has been skipped
CI / Python Bindings (maturin) (ubuntu-latest) (push) Has been skipped
CI / WASM Build + Size Check (push) Has been skipped
CI / Distributed Training Tests (push) Has been skipped
CI / CI Success (push) Failing after 1s
GPU Tests / Metal Tests (push) Has been skipped
CI / Format Check (push) Failing after 11s
CI / Build (ubuntu-latest) (push) Failing after 32s
CI / Build CPU-Only (Explicit) (push) Failing after 37s
CI / Clippy Check (push) Failing after 38s
GPU Tests / Check GPU Availability (push) Successful in 0s
GPU Tests / CUDA Tests (11.8) (push) Has been skipped
GPU Tests / CUDA Tests (12.1) (push) Has been skipped
Documentation / Build API Documentation (push) Failing after 24s
Performance Benchmarks / Run Benchmarks (push) Successful in 1m12s
Documentation / Build User Guide (push) Failing after 34s
CI / Build (macos-latest) (push) Failing after 43s
CI / Test (macos-latest) (push) Has been skipped
CI / Test (ubuntu-latest) (push) Has been skipped
CI / Python Bindings (maturin) (macos-latest) (push) Has been skipped
CI / Python Bindings (maturin) (ubuntu-latest) (push) Has been skipped
CI / WASM Build + Size Check (push) Has been skipped
CI / Distributed Training Tests (push) Has been skipped
CI / CI Success (push) Failing after 1s
GPU Tests / Metal Tests (push) Has been skipped
- Token Merging (ToMe, arXiv:2210.09461): bipartite soft matching via priority-queue
second-chance loop; apply_merge/apply_unmerge; TokenMergingLayer::forward(); 17 unit
tests + 2 doctests; 75% merge at r=32/seq=64
- Gradient accumulation per-step normalization: NormalizationStrategy
{EndOfAccumulation, PerStep, None}; with_per_step_normalization(); compute_gradient_norm()
L2 norm; PerStep divides by fixed accumulation_steps before add (not end-of-batch);
11 tests including equivalence proof vs EndOfAccumulation
- Speculative streaming: SpeculativeStreamer + mpsc::Receiver<StreamedToken>; notify_step()
sends accepted draft tokens + optional continuation immediately; StreamStats with Welford
online mean latency; collect_stream() test helper; 17 async tests
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
c57140ffe4
commit
ef3cdb1e1a
@@ -0,0 +1,524 @@
|
||||
//! Speculative streaming: yield accepted tokens immediately via channel.
|
||||
//!
|
||||
//! Standard speculative decoding collects all accepted tokens before returning
|
||||
//! them to the caller. `SpeculativeStreamer` adds a
|
||||
//! [`tokio::sync::mpsc`] channel so each accepted token is forwarded to the
|
||||
//! consumer as soon as it is produced, enabling true token-by-token streaming
|
||||
//! to an HTTP response or any other async sink.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! use rtx_inference::speculative::{
|
||||
//! SpeculativeStreamConfig, SpeculativeStreamer, collect_stream,
|
||||
//! };
|
||||
//!
|
||||
//! # #[tokio::main]
|
||||
//! # async fn main() {
|
||||
//! let config = SpeculativeStreamConfig {
|
||||
//! buffer_size: 32,
|
||||
//! include_continuation: true,
|
||||
//! ..Default::default()
|
||||
//! };
|
||||
//! let (mut streamer, rx) = SpeculativeStreamer::new(config);
|
||||
//!
|
||||
//! // Simulate two speculative steps.
|
||||
//! streamer.notify_step(&[10, 20, 30], None).await;
|
||||
//! streamer.notify_step(&[40], Some(99)).await;
|
||||
//! streamer.close();
|
||||
//!
|
||||
//! let tokens = collect_stream(rx).await;
|
||||
//! assert_eq!(tokens.len(), 5); // 3 + 1 draft + 1 continuation
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// StreamedToken
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// A single token event emitted by the speculative streaming decoder.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StreamedToken {
|
||||
/// Vocabulary token id.
|
||||
pub token_id: u32,
|
||||
/// `true` when this token came from draft acceptance; `false` when it is
|
||||
/// a continuation token sampled by the target model on rejection.
|
||||
pub from_draft: bool,
|
||||
/// Zero-based index of the speculative decode step that produced this token.
|
||||
pub step: usize,
|
||||
/// Number of tokens that had been yielded *before* this one (monotonically
|
||||
/// increasing, zero-based).
|
||||
pub total_yielded: usize,
|
||||
/// [`Instant`] at which this token was enqueued into the channel.
|
||||
pub produced_at: Instant,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SpeculativeStreamConfig
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Configuration for [`SpeculativeStreamer`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SpeculativeStreamConfig {
|
||||
/// `mpsc` channel buffer capacity.
|
||||
///
|
||||
/// A larger buffer reduces back-pressure at the cost of more memory.
|
||||
/// Default: 64.
|
||||
pub buffer_size: usize,
|
||||
/// When `true`, the continuation token (target sample emitted on partial or
|
||||
/// full rejection) is forwarded to the consumer.
|
||||
///
|
||||
/// Set `false` if the caller only wants confirmed draft tokens.
|
||||
/// Default: `true`.
|
||||
pub include_continuation: bool,
|
||||
/// How long to wait for a send to succeed when the channel is full before
|
||||
/// counting the token as dropped.
|
||||
///
|
||||
/// Default: 10 ms.
|
||||
pub send_timeout: Duration,
|
||||
}
|
||||
|
||||
impl Default for SpeculativeStreamConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
buffer_size: 64,
|
||||
include_continuation: true,
|
||||
send_timeout: Duration::from_millis(10),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// StreamStats
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Cumulative statistics for a [`SpeculativeStreamer`] session.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct StreamStats {
|
||||
/// Total number of speculative steps processed.
|
||||
pub total_steps: usize,
|
||||
/// Total tokens successfully sent to the consumer.
|
||||
pub total_yielded: usize,
|
||||
/// Tokens that were accepted from the draft model.
|
||||
pub draft_accepted: usize,
|
||||
/// Continuation tokens forwarded (target-sampled on rejection).
|
||||
pub continuation_tokens: usize,
|
||||
/// Tokens dropped because the channel was full and the send timed out.
|
||||
pub dropped_tokens: usize,
|
||||
/// Running mean of per-step wall-clock time in milliseconds.
|
||||
pub avg_step_latency_ms: f32,
|
||||
}
|
||||
|
||||
impl StreamStats {
|
||||
/// Fraction of successfully yielded tokens that came from draft acceptance.
|
||||
///
|
||||
/// Returns `0.0` when no tokens have been yielded.
|
||||
#[must_use]
|
||||
pub fn acceptance_rate(&self) -> f32 {
|
||||
if self.total_yielded == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
self.draft_accepted as f32 / self.total_yielded as f32
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SpeculativeStreamer
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Wraps a speculative decode session and yields accepted tokens immediately
|
||||
/// to a [`tokio::sync::mpsc::Receiver<StreamedToken>`].
|
||||
///
|
||||
/// Create via [`SpeculativeStreamer::new`], which returns `(streamer, receiver)`.
|
||||
/// Drive each speculative step by calling [`notify_step`][Self::notify_step].
|
||||
/// When generation is complete call [`close`][Self::close] so the receiver
|
||||
/// observes an end-of-stream.
|
||||
pub struct SpeculativeStreamer {
|
||||
config: SpeculativeStreamConfig,
|
||||
sender: mpsc::Sender<StreamedToken>,
|
||||
stats: StreamStats,
|
||||
/// Zero-based index of the *next* step to be processed.
|
||||
step: usize,
|
||||
/// Total tokens yielded so far across all steps.
|
||||
total_yielded: usize,
|
||||
}
|
||||
|
||||
impl SpeculativeStreamer {
|
||||
/// Create a new streamer.
|
||||
///
|
||||
/// Returns `(streamer, receiver)`. The receiver is the consumer-facing end
|
||||
/// of the channel; each call to [`notify_step`][Self::notify_step] sends
|
||||
/// tokens into it.
|
||||
#[must_use]
|
||||
pub fn new(config: SpeculativeStreamConfig) -> (Self, mpsc::Receiver<StreamedToken>) {
|
||||
let (tx, rx) = mpsc::channel(config.buffer_size);
|
||||
let streamer = Self {
|
||||
config,
|
||||
sender: tx,
|
||||
stats: StreamStats::default(),
|
||||
step: 0,
|
||||
total_yielded: 0,
|
||||
};
|
||||
(streamer, rx)
|
||||
}
|
||||
|
||||
/// Process one speculative decode step.
|
||||
///
|
||||
/// Sends each accepted draft token and (optionally) the continuation token
|
||||
/// to the channel immediately. A send that times out (channel full for
|
||||
/// longer than [`SpeculativeStreamConfig::send_timeout`]) is counted in
|
||||
/// [`StreamStats::dropped_tokens`] and silently skipped.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `accepted` — slice of token ids accepted from the draft model in this step.
|
||||
/// - `continuation` — optional target-sampled continuation token (present when
|
||||
/// the draft was partially or fully rejected).
|
||||
///
|
||||
/// # Returns
|
||||
/// Number of tokens successfully sent to the consumer in this call.
|
||||
pub async fn notify_step(&mut self, accepted: &[u32], continuation: Option<u32>) -> usize {
|
||||
let step_start = Instant::now();
|
||||
let mut sent: usize = 0;
|
||||
|
||||
for &token_id in accepted {
|
||||
let event = StreamedToken {
|
||||
token_id,
|
||||
from_draft: true,
|
||||
step: self.step,
|
||||
total_yielded: self.total_yielded,
|
||||
produced_at: Instant::now(),
|
||||
};
|
||||
if self.try_send(event).await {
|
||||
self.total_yielded += 1;
|
||||
self.stats.draft_accepted += 1;
|
||||
self.stats.total_yielded += 1;
|
||||
sent += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if self.config.include_continuation {
|
||||
if let Some(token_id) = continuation {
|
||||
let event = StreamedToken {
|
||||
token_id,
|
||||
from_draft: false,
|
||||
step: self.step,
|
||||
total_yielded: self.total_yielded,
|
||||
produced_at: Instant::now(),
|
||||
};
|
||||
if self.try_send(event).await {
|
||||
self.total_yielded += 1;
|
||||
self.stats.continuation_tokens += 1;
|
||||
self.stats.total_yielded += 1;
|
||||
sent += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update running mean of per-step wall-clock latency.
|
||||
let elapsed_ms = step_start.elapsed().as_secs_f32() * 1_000.0;
|
||||
// `self.step` is the 0-based index of the step we just processed.
|
||||
// After incrementing below it becomes `n`, the total steps done.
|
||||
// We compute the new mean using the Welford online update:
|
||||
// new_mean = old_mean + (x - old_mean) / n
|
||||
let n = (self.step + 1) as f32;
|
||||
self.stats.avg_step_latency_ms += (elapsed_ms - self.stats.avg_step_latency_ms) / n;
|
||||
|
||||
self.step += 1;
|
||||
self.stats.total_steps += 1;
|
||||
sent
|
||||
}
|
||||
|
||||
/// Close the stream.
|
||||
///
|
||||
/// Drops the sender so the receiver drains to empty and then observes
|
||||
/// end-of-stream (`recv()` returns `None`).
|
||||
pub fn close(self) {
|
||||
// `self` is consumed; dropping it drops `self.sender`.
|
||||
}
|
||||
|
||||
/// Return a reference to the current streaming statistics.
|
||||
#[must_use]
|
||||
pub fn stats(&self) -> &StreamStats {
|
||||
&self.stats
|
||||
}
|
||||
|
||||
/// Zero-based index of the *next* step to be processed.
|
||||
///
|
||||
/// After `n` calls to [`notify_step`] this returns `n`.
|
||||
#[must_use]
|
||||
pub fn step(&self) -> usize {
|
||||
self.step
|
||||
}
|
||||
|
||||
/// Total number of tokens successfully yielded across all completed steps.
|
||||
#[must_use]
|
||||
pub fn total_yielded(&self) -> usize {
|
||||
self.total_yielded
|
||||
}
|
||||
|
||||
/// Returns `true` if the consumer has dropped the receiver end of the
|
||||
/// channel, meaning further sends will fail.
|
||||
#[must_use]
|
||||
pub fn is_closed(&self) -> bool {
|
||||
self.sender.is_closed()
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Private helpers
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// Attempt to send one token event within the configured timeout.
|
||||
///
|
||||
/// Returns `true` on success, `false` if the channel is full and the
|
||||
/// timeout elapses or the receiver has been dropped.
|
||||
async fn try_send(&mut self, event: StreamedToken) -> bool {
|
||||
match tokio::time::timeout(self.config.send_timeout, self.sender.send(event)).await {
|
||||
Ok(Ok(())) => true,
|
||||
// `Ok(Err(_))` — receiver dropped; `Err(_)` — timeout.
|
||||
Ok(Err(_)) | Err(_) => {
|
||||
self.stats.dropped_tokens += 1;
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Utility: collect_stream
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Drain a [`StreamedToken`] receiver until the sender is closed.
|
||||
///
|
||||
/// Useful in tests and batch collection scenarios where you want all tokens as
|
||||
/// a `Vec` rather than processing them one at a time.
|
||||
///
|
||||
/// ```rust
|
||||
/// use rtx_inference::speculative::{SpeculativeStreamConfig, SpeculativeStreamer, collect_stream};
|
||||
///
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() {
|
||||
/// let (mut streamer, rx) = SpeculativeStreamer::new(Default::default());
|
||||
/// streamer.notify_step(&[1, 2, 3], None).await;
|
||||
/// streamer.close();
|
||||
/// let all = collect_stream(rx).await;
|
||||
/// assert_eq!(all.len(), 3);
|
||||
/// # }
|
||||
/// ```
|
||||
pub async fn collect_stream(mut rx: mpsc::Receiver<StreamedToken>) -> Vec<StreamedToken> {
|
||||
let mut tokens = Vec::new();
|
||||
while let Some(t) = rx.recv().await {
|
||||
tokens.push(t);
|
||||
}
|
||||
tokens
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// Helper: config with no continuation tokens and a small buffer.
|
||||
fn cfg_no_cont() -> SpeculativeStreamConfig {
|
||||
SpeculativeStreamConfig {
|
||||
buffer_size: 16,
|
||||
include_continuation: false,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn cfg_with_cont() -> SpeculativeStreamConfig {
|
||||
SpeculativeStreamConfig {
|
||||
buffer_size: 16,
|
||||
include_continuation: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_streamer_yields_accepted_tokens() {
|
||||
let (mut streamer, rx) = SpeculativeStreamer::new(cfg_no_cont());
|
||||
let sent = streamer.notify_step(&[10, 20, 30], None).await;
|
||||
assert_eq!(sent, 3);
|
||||
streamer.close();
|
||||
let collected = collect_stream(rx).await;
|
||||
assert_eq!(collected.len(), 3);
|
||||
assert_eq!(collected[0].token_id, 10);
|
||||
assert!(collected[0].from_draft);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_streamer_includes_continuation_token() {
|
||||
let (mut streamer, rx) = SpeculativeStreamer::new(cfg_with_cont());
|
||||
let sent = streamer.notify_step(&[1, 2], Some(99)).await;
|
||||
assert_eq!(sent, 3); // 2 draft + 1 continuation
|
||||
streamer.close();
|
||||
let collected = collect_stream(rx).await;
|
||||
assert_eq!(collected.len(), 3);
|
||||
assert!(!collected[2].from_draft);
|
||||
assert_eq!(collected[2].token_id, 99);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_streamer_excludes_continuation_when_disabled() {
|
||||
let (mut streamer, rx) = SpeculativeStreamer::new(cfg_no_cont());
|
||||
streamer.notify_step(&[5], Some(42)).await;
|
||||
streamer.close();
|
||||
let collected = collect_stream(rx).await;
|
||||
assert_eq!(collected.len(), 1);
|
||||
assert_eq!(collected[0].token_id, 5);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_streamer_step_counter_increments() {
|
||||
let (mut streamer, _rx) = SpeculativeStreamer::new(Default::default());
|
||||
assert_eq!(streamer.step(), 0);
|
||||
streamer.notify_step(&[1], None).await;
|
||||
assert_eq!(streamer.step(), 1);
|
||||
streamer.notify_step(&[2, 3], Some(4)).await;
|
||||
assert_eq!(streamer.step(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_streamer_total_yielded_accumulates() {
|
||||
let (mut streamer, _rx) = SpeculativeStreamer::new(cfg_with_cont());
|
||||
streamer.notify_step(&[1, 2], Some(3)).await; // 3 tokens
|
||||
assert_eq!(streamer.total_yielded(), 3);
|
||||
streamer.notify_step(&[4], None).await; // 1 more
|
||||
assert_eq!(streamer.total_yielded(), 4);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stream_stats_acceptance_rate() {
|
||||
let (mut streamer, _rx) = SpeculativeStreamer::new(cfg_with_cont());
|
||||
streamer.notify_step(&[1, 2], Some(3)).await; // 2 draft, 1 continuation
|
||||
let stats = streamer.stats();
|
||||
// 2 draft / 3 total ≈ 0.667
|
||||
let rate = stats.acceptance_rate();
|
||||
assert!(
|
||||
(rate - 2.0_f32 / 3.0).abs() < 0.01,
|
||||
"acceptance rate {rate}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stream_collect_empty_steps() {
|
||||
let (mut streamer, rx) = SpeculativeStreamer::new(Default::default());
|
||||
streamer.notify_step(&[], None).await; // empty step — nothing sent
|
||||
streamer.close();
|
||||
let collected = collect_stream(rx).await;
|
||||
assert_eq!(collected.len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_is_closed_after_receiver_dropped() {
|
||||
let (streamer, rx) = SpeculativeStreamer::new(Default::default());
|
||||
drop(rx);
|
||||
assert!(streamer.is_closed());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_multi_step_ordering() {
|
||||
let (mut streamer, rx) = SpeculativeStreamer::new(Default::default());
|
||||
streamer.notify_step(&[10, 11], None).await;
|
||||
streamer.notify_step(&[20, 21], None).await;
|
||||
streamer.close();
|
||||
let collected = collect_stream(rx).await;
|
||||
assert_eq!(collected.len(), 4);
|
||||
assert_eq!(collected[0].step, 0);
|
||||
assert_eq!(collected[1].step, 0);
|
||||
assert_eq!(collected[2].step, 1);
|
||||
assert_eq!(collected[3].step, 1);
|
||||
}
|
||||
|
||||
// Additional tests for thorough coverage.
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_token_ids_preserved_in_order() {
|
||||
let (mut streamer, rx) = SpeculativeStreamer::new(cfg_no_cont());
|
||||
streamer.notify_step(&[100, 200, 300, 400], None).await;
|
||||
streamer.close();
|
||||
let collected = collect_stream(rx).await;
|
||||
let ids: Vec<u32> = collected.iter().map(|t| t.token_id).collect();
|
||||
assert_eq!(ids, vec![100, 200, 300, 400]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_total_yielded_field_in_event_matches_count() {
|
||||
let (mut streamer, rx) = SpeculativeStreamer::new(cfg_no_cont());
|
||||
streamer.notify_step(&[1, 2, 3], None).await;
|
||||
streamer.close();
|
||||
let collected = collect_stream(rx).await;
|
||||
// The `total_yielded` field inside each event is the cumulative count
|
||||
// *before* that token was emitted.
|
||||
assert_eq!(collected[0].total_yielded, 0);
|
||||
assert_eq!(collected[1].total_yielded, 1);
|
||||
assert_eq!(collected[2].total_yielded, 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stats_total_steps_increments() {
|
||||
let (mut streamer, _rx) = SpeculativeStreamer::new(Default::default());
|
||||
streamer.notify_step(&[1], None).await;
|
||||
streamer.notify_step(&[2], None).await;
|
||||
assert_eq!(streamer.stats().total_steps, 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stats_continuation_count() {
|
||||
let (mut streamer, _rx) = SpeculativeStreamer::new(cfg_with_cont());
|
||||
streamer.notify_step(&[], Some(7)).await;
|
||||
streamer.notify_step(&[], Some(8)).await;
|
||||
let stats = streamer.stats();
|
||||
assert_eq!(stats.continuation_tokens, 2);
|
||||
assert_eq!(stats.draft_accepted, 0);
|
||||
assert_eq!(stats.total_yielded, 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_acceptance_rate_zero_when_no_tokens() {
|
||||
let (streamer, _rx) = SpeculativeStreamer::new(Default::default());
|
||||
assert_eq!(streamer.stats().acceptance_rate(), 0.0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_avg_step_latency_populated() {
|
||||
let (mut streamer, _rx) = SpeculativeStreamer::new(Default::default());
|
||||
streamer.notify_step(&[1, 2], None).await;
|
||||
streamer.notify_step(&[3], None).await;
|
||||
// We cannot assert an exact value, but it must be non-negative.
|
||||
assert!(streamer.stats().avg_step_latency_ms >= 0.0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_drop_when_receiver_gone() {
|
||||
// Buffer of 1 so the second token must block, but receiver is gone.
|
||||
let config = SpeculativeStreamConfig {
|
||||
buffer_size: 1,
|
||||
include_continuation: false,
|
||||
send_timeout: Duration::from_millis(1),
|
||||
};
|
||||
let (mut streamer, rx) = SpeculativeStreamer::new(config);
|
||||
drop(rx); // consumer gone immediately
|
||||
// All sends should fail gracefully — no panic.
|
||||
let sent = streamer.notify_step(&[1, 2, 3], None).await;
|
||||
assert_eq!(sent, 0);
|
||||
assert_eq!(streamer.stats().dropped_tokens, 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_collect_stream_helper() {
|
||||
let (mut streamer, rx) = SpeculativeStreamer::new(cfg_with_cont());
|
||||
streamer.notify_step(&[10, 20], Some(30)).await;
|
||||
streamer.close();
|
||||
let all = collect_stream(rx).await;
|
||||
assert_eq!(all.len(), 3);
|
||||
assert_eq!(all[2].token_id, 30);
|
||||
assert!(!all[2].from_draft);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user