Skip to main content

optirs_core/streaming/
low_latency.rs

1// Low-latency optimization for real-time streaming applications
2//
3// This module provides specialized optimizers and techniques for applications
4// that require extremely low latency updates, such as high-frequency trading,
5// real-time control systems, and interactive machine learning.
6
7use scirs2_core::ndarray::Array1;
8use scirs2_core::numeric::Float;
9use std::collections::VecDeque;
10use std::sync::{
11    atomic::{AtomicUsize, Ordering},
12    Arc, Mutex, MutexGuard, PoisonError,
13};
14use std::time::{Duration, Instant};
15
16use crate::error::{OptimError, Result};
17use crate::optimizers::Optimizer;
18
19#[cfg(test)]
20mod regression_tests;
21
22/// Recovers a mutex guard even if the lock was poisoned by a panicking thread.
23///
24/// The data protected by every mutex in this module is a plain value with no
25/// cross-field invariant that a panic could leave half-updated, so continuing
26/// with the recovered value is strictly better than panicking a real-time
27/// update path.
28fn lock_recovered<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
29    mutex.lock().unwrap_or_else(PoisonError::into_inner)
30}
31
32/// Low-latency optimization configuration
33#[derive(Debug, Clone)]
34pub struct LowLatencyConfig {
35    /// Target latency budget (microseconds)
36    pub target_latency_us: u64,
37
38    /// Maximum acceptable latency (microseconds)
39    pub max_latency_us: u64,
40
41    /// Enable pre-computation of updates
42    pub enable_precomputation: bool,
43
44    /// Buffer size for pre-computed updates
45    pub precomputation_buffer_size: usize,
46
47    /// Enable lock-free algorithms
48    pub enable_lock_free: bool,
49
50    /// Use approximate algorithms for speed
51    pub use_approximations: bool,
52
53    /// Approximation tolerance
54    pub approximation_tolerance: f64,
55
56    /// Enable SIMD optimizations
57    pub enable_simd: bool,
58
59    /// Batch processing threshold
60    pub batch_threshold: usize,
61
62    /// Enable zero-copy operations
63    pub enable_zero_copy: bool,
64
65    /// Memory pool size for allocations
66    pub memory_pool_size: usize,
67
68    /// Enable gradient quantization
69    pub enable_quantization: bool,
70
71    /// Quantization bits
72    pub quantization_bits: u8,
73}
74
75impl Default for LowLatencyConfig {
76    fn default() -> Self {
77        Self {
78            target_latency_us: 100, // 100 microseconds
79            max_latency_us: 1000,   // 1 millisecond
80            enable_precomputation: true,
81            precomputation_buffer_size: 64,
82            enable_lock_free: true,
83            use_approximations: true,
84            approximation_tolerance: 0.01,
85            enable_simd: true,
86            batch_threshold: 8,
87            enable_zero_copy: true,
88            memory_pool_size: 1024 * 1024, // 1MB
89            enable_quantization: false,
90            quantization_bits: 8,
91        }
92    }
93}
94
95/// Low-latency streaming optimizer
96pub struct LowLatencyOptimizer<O, A>
97where
98    A: Float + Send + Sync + scirs2_core::ndarray::ScalarOperand + std::fmt::Debug,
99    O: Optimizer<A, scirs2_core::ndarray::Ix1> + Send + Sync,
100{
101    /// Base optimizer
102    base_optimizer: Arc<Mutex<O>>,
103
104    /// Configuration
105    config: LowLatencyConfig,
106
107    /// Live parameter vector (L1).
108    ///
109    /// The optimizer keeps the parameters it is optimizing so that every step
110    /// is applied to the *result of the previous step*. Previously each step
111    /// created a fresh `Array1::zeros(..)` as "current parameters", which meant
112    /// the returned vector was always a single step away from the origin: the
113    /// optimizer silently discarded all accumulated progress and, for any
114    /// caller that stored the return value, effectively zeroed the parameters
115    /// on every update. Seed real initial weights with [`Self::set_parameters`];
116    /// if nothing is seeded the vector starts at the origin on the first step.
117    parameters: Option<Array1<A>>,
118
119    /// Pre-computation engine
120    precomputation_engine: Option<PrecomputationEngine<A>>,
121
122    /// Bounded staging ring for produced updates
123    update_buffer: LockFreeBuffer<A>,
124
125    /// Memory pool for fast allocations
126    memory_pool: FastMemoryPool<A>,
127
128    /// Chunked vector processor
129    simd_processor: SIMDProcessor<A>,
130
131    /// Quantization engine
132    quantizer: Option<GradientQuantizer<A>>,
133
134    /// Performance monitor
135    perf_monitor: LatencyMonitor,
136
137    /// Approximation controller
138    approximation_controller: ApproximationController<A>,
139
140    /// Step counter (atomic for thread safety)
141    step_counter: AtomicUsize,
142}
143
144/// Pre-computation engine for preparing updates in advance
145struct PrecomputationEngine<A: Float + Send + Sync> {
146    /// Buffer of pre-computed updates
147    precomputed_updates: VecDeque<PrecomputedUpdate<A>>,
148
149    /// Prediction model for future gradients
150    gradient_predictor: GradientPredictor<A>,
151
152    /// Maximum buffer size
153    max_buffer_size: usize,
154
155    /// Number of steps that were served from a pre-computed update
156    hits: usize,
157
158    /// Number of steps that had to fall back to a full update
159    misses: usize,
160
161    /// Minimum recorded prediction confidence a pre-computed update must carry
162    /// to be served.
163    ///
164    /// The predictor's measured confidence was stored on every entry and then
165    /// never consulted, so a wild guess was served as readily as a well
166    /// -supported prediction. Zero (the default) preserves that behaviour;
167    /// raising it makes the engine fall back to a full update when the
168    /// predictor is unsure.
169    min_confidence: A,
170}
171
172/// Pre-computed update entry
173#[derive(Debug, Clone)]
174struct PrecomputedUpdate<A: Float + Send + Sync> {
175    /// Predicted gradient
176    gradient: Array1<A>,
177
178    /// Pre-computed parameter update
179    update: Array1<A>,
180
181    /// Validity timestamp
182    valid_until: Instant,
183
184    /// Confidence score
185    confidence: A,
186}
187
188/// Bounded staging ring for produced updates.
189///
190/// Accessed exclusively through `&mut self` from the owning optimizer, so it
191/// needs no locking at all — hence "lock free". It is deliberately *not* a
192/// concurrent MPMC queue: claiming that would require `unsafe` interior
193/// mutability this module does not want in a real-time path.
194struct LockFreeBuffer<A: Float + Send + Sync> {
195    /// Buffer storage
196    buffer: Vec<Option<Array1<A>>>,
197
198    /// Write index (atomic)
199    write_index: AtomicUsize,
200
201    /// Read index (atomic)
202    read_index: AtomicUsize,
203
204    /// Buffer capacity
205    capacity: usize,
206}
207
208/// Fast memory pool for low-latency allocations.
209///
210/// L5: the previous implementation held `Vec<*mut u8>` filled by
211/// `std::alloc::alloc` with no `Drop`, so every dropped optimizer leaked its
212/// whole pool (1 MB by default). Blocks are now owned `Vec<A>` buffers, which
213/// release themselves when the pool is dropped — the leak is fixed by
214/// ownership rather than by a hand-written `Drop`, and the module no longer
215/// contains any `unsafe` code.
216struct FastMemoryPool<A> {
217    /// Currently free blocks, each pre-allocated to `elements_per_block`
218    free_blocks: Mutex<Vec<Vec<A>>>,
219
220    /// Elements per block
221    elements_per_block: usize,
222
223    /// Total blocks the pool was created with
224    total_blocks: usize,
225
226    /// Blocks currently checked out
227    checked_out: AtomicUsize,
228
229    /// High-water mark of simultaneously checked-out blocks
230    peak_checked_out: AtomicUsize,
231
232    /// Requests the pool could not satisfy (caller had to allocate)
233    misses: AtomicUsize,
234}
235
236/// Chunked vector processor for the fast update path
237struct SIMDProcessor<A: Float + Send + Sync> {
238    /// Enable chunked processing
239    enabled: bool,
240
241    /// Chunk width used when walking the contiguous parameter slice
242    vector_width: usize,
243
244    /// Marker so the processor stays tied to the element type
245    _element: std::marker::PhantomData<A>,
246}
247
248/// Gradient quantization for reduced precision
249struct GradientQuantizer<A: Float + Send + Sync> {
250    /// Quantization bits
251    bits: u8,
252
253    /// Quantization scale
254    scale: A,
255
256    /// Zero point
257    zero_point: A,
258
259    /// Quantization error accumulator (error feedback)
260    error_accumulator: Option<Array1<A>>,
261}
262
263/// Latency monitoring and profiling
264#[derive(Debug)]
265struct LatencyMonitor {
266    /// Recent latency samples
267    latency_samples: VecDeque<Duration>,
268
269    /// Maximum samples to keep
270    maxsamples: usize,
271
272    /// Current percentiles
273    p50_latency: Duration,
274    p95_latency: Duration,
275    p99_latency: Duration,
276
277    /// Violation count
278    violations: usize,
279
280    /// Total operations
281    total_operations: usize,
282}
283
284/// Maximum age of a retained latency/accuracy measurement.
285const PERFORMANCE_WINDOW_AGE: Duration = Duration::from_secs(30);
286
287/// Maximum number of retained latency/accuracy measurements.
288const PERFORMANCE_WINDOW_LEN: usize = 100;
289
290/// Approximation controller for trading accuracy for speed
291struct ApproximationController<A: Float + Send + Sync> {
292    /// Current approximation level (0.0 = exact, 1.0 = maximum approximation)
293    approximation_level: A,
294
295    /// Performance history
296    performance_history: VecDeque<PerformancePoint<A>>,
297
298    /// Adaptation rate
299    adaptation_rate: A,
300
301    /// Target latency
302    targetlatency: Duration,
303}
304
305/// Performance measurement point
306#[derive(Debug, Clone)]
307struct PerformancePoint<A: Float + Send + Sync> {
308    /// Latency measurement
309    latency: Duration,
310
311    /// Accuracy achieved
312    accuracy: A,
313
314    /// Timestamp
315    timestamp: Instant,
316}
317
318/// Gradient predictor for pre-computation
319struct GradientPredictor<A: Float + Send + Sync> {
320    /// Recent gradient history
321    gradient_history: VecDeque<Array1<A>>,
322
323    /// Per-coordinate least-squares slope of the observed history
324    trend_weights: Option<Array1<A>>,
325
326    /// History window size
327    windowsize: usize,
328
329    /// Measured prediction confidence (EWMA of cosine similarity between the
330    /// last prediction and the gradient that actually arrived). `None` until
331    /// at least one prediction has been scored against real data — the
332    /// confidence is never seeded with an invented number.
333    confidence: Option<A>,
334
335    /// The prediction currently awaiting a real observation
336    pending_prediction: Option<Array1<A>>,
337}
338
339impl<O, A> LowLatencyOptimizer<O, A>
340where
341    A: Float
342        + Send
343        + Sync
344        + Default
345        + Clone
346        + std::fmt::Debug
347        + scirs2_core::ndarray::ScalarOperand
348        + 'static
349        + std::iter::Sum,
350    O: Optimizer<A, scirs2_core::ndarray::Ix1> + Send + Sync + 'static,
351{
352    /// Create a new low-latency optimizer
353    pub fn new(_baseoptimizer: O, config: LowLatencyConfig) -> Result<Self> {
354        let base_optimizer = Arc::new(Mutex::new(_baseoptimizer));
355
356        let precomputation_engine = if config.enable_precomputation {
357            Some(PrecomputationEngine::new(config.precomputation_buffer_size))
358        } else {
359            None
360        };
361
362        let update_buffer = LockFreeBuffer::new(config.precomputation_buffer_size);
363        let memory_pool = FastMemoryPool::new(config.memory_pool_size, 4096)?; // 4KB blocks
364        let simd_processor = SIMDProcessor::new(config.enable_simd, config.batch_threshold);
365
366        let quantizer = if config.enable_quantization {
367            Some(GradientQuantizer::new(config.quantization_bits))
368        } else {
369            None
370        };
371
372        let perf_monitor = LatencyMonitor::new(1000); // Keep 1000 samples
373        let approximation_controller =
374            ApproximationController::new(Duration::from_micros(config.target_latency_us));
375
376        Ok(Self {
377            base_optimizer,
378            config,
379            parameters: None,
380            precomputation_engine,
381            update_buffer,
382            memory_pool,
383            simd_processor,
384            quantizer,
385            perf_monitor,
386            approximation_controller,
387            step_counter: AtomicUsize::new(0),
388        })
389    }
390
391    /// Seed the parameter vector the optimizer will keep updating.
392    pub fn set_parameters(&mut self, parameters: Array1<A>) {
393        self.parameters = Some(parameters);
394    }
395
396    /// Current parameter vector, if any step has been taken or seeded.
397    /// Require a minimum predictor confidence before a pre-computed update is
398    /// served, falling back to a full update below it.
399    ///
400    /// No-op when pre-computation is disabled. Defaults to zero, which accepts
401    /// any prediction that matches the arriving gradient.
402    pub fn set_precomputation_min_confidence(&mut self, min_confidence: A) {
403        if let Some(precomp) = self.precomputation_engine.as_mut() {
404            precomp.set_min_confidence(min_confidence);
405        }
406    }
407
408    pub fn parameters(&self) -> Option<&Array1<A>> {
409        self.parameters.as_ref()
410    }
411
412    /// Perform a low-latency update
413    pub fn low_latency_step(&mut self, gradient: &Array1<A>) -> Result<Array1<A>> {
414        let start_time = Instant::now();
415
416        if gradient.is_empty() {
417            return Err(OptimError::DimensionMismatch(
418                "low_latency_step received an empty gradient".to_string(),
419            ));
420        }
421
422        let previous_params = self.parameters.clone();
423        let learning_rate = self.base_learning_rate();
424        let tolerance = self.config.approximation_tolerance.max(0.0);
425
426        // Speculative fast path: a pre-computed update is only served when the
427        // gradient it was computed for actually matches the gradient that
428        // arrived, within `approximation_tolerance`. That check is what makes
429        // the reported hit rate a real measurement instead of a constant.
430        let served = self
431            .precomputation_engine
432            .as_mut()
433            .and_then(|precomp| precomp.try_get_precomputed(gradient, tolerance));
434        if let Some(precomputed) = served {
435            let update = precomputed.update;
436            self.parameters = Some(update.clone());
437            let latency = start_time.elapsed();
438            self.perf_monitor.record_latency(latency);
439            if self.config.enable_lock_free {
440                self.update_buffer.push(update.clone());
441            }
442            let validity = Duration::from_micros(self.config.max_latency_us.max(1));
443            if let Some(ref mut precomp) = self.precomputation_engine {
444                precomp.start_precomputation(gradient, &update, learning_rate, validity);
445            }
446            self.step_counter.fetch_add(1, Ordering::Relaxed);
447            return Ok(update);
448        }
449
450        // Quantize gradient if enabled. With zero-copy enabled and no
451        // quantizer configured the original gradient is used in place, so the
452        // hot path performs no defensive clone at all.
453        let quantized = match self.quantizer.as_mut() {
454            Some(quantizer) => Some(quantizer.quantize(gradient)?),
455            None if self.config.enable_zero_copy => None,
456            None => Some(gradient.clone()),
457        };
458        let processed_gradient: &Array1<A> = quantized.as_ref().unwrap_or(gradient);
459
460        // Use approximation if necessary to meet latency budget
461        let approximation_level = self.approximation_controller.get_approximation_level();
462        let use_approximation = self.config.use_approximations && approximation_level > A::zero();
463        let update = if use_approximation {
464            let simplified = self.simplify_gradient(processed_gradient, approximation_level)?;
465            self.fast_path_update(&simplified, learning_rate)?
466        } else {
467            self.exact_update(processed_gradient)?
468        };
469
470        let latency = start_time.elapsed();
471
472        // Record performance and adapt approximation level
473        let accuracy = Self::estimate_accuracy(previous_params.as_ref(), &update, gradient);
474        self.approximation_controller
475            .record_performance(latency, approximation_level, accuracy);
476        self.perf_monitor.record_latency(latency);
477
478        // Check for latency violations
479        if latency.as_micros() as u64 > self.config.max_latency_us {
480            self.handle_latency_violation(latency)?;
481        }
482
483        // Stage the produced update for asynchronous consumers.
484        if self.config.enable_lock_free {
485            self.update_buffer.push(update.clone());
486        }
487
488        // Prepare the next step's speculative update while the caller is busy
489        // fetching its next sample.
490        let validity = Duration::from_micros(self.config.max_latency_us.max(1));
491        if let Some(ref mut precomp) = self.precomputation_engine {
492            precomp.start_precomputation(gradient, &update, learning_rate, validity);
493        }
494
495        self.parameters = Some(update.clone());
496        self.step_counter.fetch_add(1, Ordering::Relaxed);
497        Ok(update)
498    }
499
500    /// Learning rate currently configured on the wrapped optimizer.
501    fn base_learning_rate(&self) -> A {
502        lock_recovered(&self.base_optimizer).get_learning_rate()
503    }
504
505    /// Parameter vector to step from, allocated at the origin on first use.
506    fn current_parameters(&self, len: usize) -> Result<Array1<A>> {
507        match self.parameters.as_ref() {
508            Some(params) if params.len() == len => Ok(params.clone()),
509            Some(params) => Err(OptimError::DimensionMismatch(format!(
510                "gradient has {} elements but the tracked parameters have {}",
511                len,
512                params.len()
513            ))),
514            None => Ok(Array1::zeros(len)),
515        }
516    }
517
518    /// Perform exact update using base optimizer
519    fn exact_update(&mut self, gradient: &Array1<A>) -> Result<Array1<A>> {
520        let current_params = self.current_parameters(gradient.len())?;
521        let mut optimizer = lock_recovered(&self.base_optimizer);
522        optimizer.step(&current_params, gradient)
523    }
524
525    /// Chunked first-order update used by the approximate / pre-computation
526    /// paths.
527    ///
528    /// L3: this used to hand the gradient to a "SIMD processor" that returned
529    /// `gradient.clone()`, so the approximate path returned the *gradient*
530    /// where the caller expected *new parameters* and applied no update at
531    /// all. It now performs a real chunked `params -= lr * gradient` walk over
532    /// the contiguous parameter slice.
533    fn fast_path_update(&mut self, gradient: &Array1<A>, learning_rate: A) -> Result<Array1<A>> {
534        if !self.simd_processor.is_active(gradient.len()) {
535            return self.exact_update(gradient);
536        }
537        let mut params = self.current_parameters(gradient.len())?;
538        self.simd_processor
539            .apply_scaled_subtract(&mut params, gradient, learning_rate);
540        Ok(params)
541    }
542
543    /// Simplify gradient for approximation by keeping the largest magnitudes.
544    fn simplify_gradient(&self, gradient: &Array1<A>, level: A) -> Result<Array1<A>> {
545        let n = gradient.len();
546        if n == 0 {
547            return Ok(gradient.clone());
548        }
549
550        let sparsity_ratio = level.to_f64().unwrap_or(0.0).clamp(0.0, 1.0);
551        let keep_ratio = 1.0 - sparsity_ratio * 0.8; // Keep 20% to 100% of gradients
552        let keep_count = (((n as f64) * keep_ratio).round() as usize).clamp(1, n);
553        if keep_count == n {
554            return Ok(gradient.clone());
555        }
556
557        // Magnitudes go into a pooled scratch buffer so the hot path does not
558        // allocate, and the k-th largest magnitude is found in linear time
559        // instead of by fully sorting.
560        let mut magnitudes = self
561            .memory_pool
562            .acquire(n)
563            .unwrap_or_else(|| Vec::with_capacity(n));
564        magnitudes.clear();
565        magnitudes.extend(gradient.iter().map(|g| g.abs()));
566        let kth = keep_count - 1;
567        magnitudes.select_nth_unstable_by(kth, |a, b| {
568            b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
569        });
570        let threshold = magnitudes[kth];
571        self.memory_pool.release(magnitudes);
572
573        let mut simplified = Array1::zeros(n);
574        let mut kept = 0usize;
575        for (i, &g) in gradient.iter().enumerate() {
576            if kept < keep_count && g.abs() >= threshold {
577                simplified[i] = g;
578                kept += 1;
579            }
580        }
581
582        Ok(simplified)
583    }
584
585    /// Estimate how well the applied step follows the true descent direction.
586    ///
587    /// The previous version compared the *new parameter vector* with the
588    /// *gradient*, two quantities with no meaningful angle between them. The
589    /// meaningful comparison is between the applied delta and `-gradient`.
590    fn estimate_accuracy(
591        previous_params: Option<&Array1<A>>,
592        new_params: &Array1<A>,
593        gradient: &Array1<A>,
594    ) -> A {
595        if new_params.len() != gradient.len() {
596            return A::zero();
597        }
598
599        let zeros = Array1::zeros(new_params.len());
600        let previous = match previous_params {
601            Some(previous) if previous.len() == new_params.len() => previous,
602            _ => &zeros,
603        };
604
605        let mut dot = A::zero();
606        let mut norm_delta = A::zero();
607        let mut norm_grad = A::zero();
608        for ((&p_new, &p_old), &g) in new_params.iter().zip(previous.iter()).zip(gradient.iter()) {
609            let delta = p_new - p_old;
610            dot = dot + delta * (-g);
611            norm_delta = norm_delta + delta * delta;
612            norm_grad = norm_grad + g * g;
613        }
614
615        let norm_delta = norm_delta.sqrt();
616        let norm_grad = norm_grad.sqrt();
617        if norm_delta == A::zero() || norm_grad == A::zero() {
618            A::zero()
619        } else {
620            dot / (norm_delta * norm_grad)
621        }
622    }
623
624    /// Handle latency violations
625    fn handle_latency_violation(&mut self, latency: Duration) -> Result<()> {
626        // Record the violation so `LowLatencyMetrics::latency_violations` is a
627        // real count rather than a permanent zero.
628        self.perf_monitor.violations += 1;
629
630        // Increase approximation level to reduce future latency
631        self.approximation_controller.increase_approximation();
632
633        // Escalate to gradient quantization when the budget is being missed by
634        // a wide margin and quantization has not been enabled yet.
635        if !self.config.enable_quantization
636            && latency.as_micros() as u64 > self.config.max_latency_us.saturating_mul(2)
637        {
638            self.config.enable_quantization = true;
639            self.quantizer = Some(GradientQuantizer::new(self.config.quantization_bits));
640        }
641
642        Ok(())
643    }
644
645    /// Take the oldest staged update, if any.
646    pub fn try_pop_staged_update(&mut self) -> Option<Array1<A>> {
647        self.update_buffer.pop()
648    }
649
650    /// Number of updates currently staged.
651    pub fn staged_update_count(&self) -> usize {
652        self.update_buffer.len()
653    }
654
655    /// Get current performance metrics
656    pub fn get_performance_metrics(&self) -> LowLatencyMetrics {
657        LowLatencyMetrics {
658            avg_latency_us: self.perf_monitor.get_average_latency().as_micros() as u64,
659            p50_latency_us: self.perf_monitor.p50_latency.as_micros() as u64,
660            p95_latency_us: self.perf_monitor.p95_latency.as_micros() as u64,
661            p99_latency_us: self.perf_monitor.p99_latency.as_micros() as u64,
662            latency_violations: self.perf_monitor.violations,
663            total_operations: self.perf_monitor.total_operations,
664            current_approximation_level: self
665                .approximation_controller
666                .approximation_level
667                .to_f64()
668                .unwrap_or(0.0),
669            approximation_accuracy: self
670                .approximation_controller
671                .mean_accuracy()
672                .and_then(|value| value.to_f64()),
673            precomputation_hit_rate: self
674                .precomputation_engine
675                .as_ref()
676                .and_then(|pe| pe.hit_rate()),
677            precomputation_attempts: self
678                .precomputation_engine
679                .as_ref()
680                .map(|pe| pe.attempts())
681                .unwrap_or(0),
682            memory_efficiency: self.memory_pool.get_efficiency(),
683            memory_pool_misses: self.memory_pool.misses(),
684        }
685    }
686
687    /// Check if optimizer is meeting latency requirements
688    pub fn is_meeting_latency_requirements(&self) -> bool {
689        let avg_latency = self.perf_monitor.get_average_latency().as_micros() as u64;
690        avg_latency <= self.config.target_latency_us
691    }
692}
693
694// Implementation of helper structs
695impl<A: Float + Send + Sync + std::iter::Sum> PrecomputationEngine<A> {
696    fn new(_buffersize: usize) -> Self {
697        let capacity = _buffersize.max(1);
698        Self {
699            precomputed_updates: VecDeque::with_capacity(capacity),
700            gradient_predictor: GradientPredictor::new(10), // 10-step history
701            max_buffer_size: capacity,
702            hits: 0,
703            misses: 0,
704            min_confidence: A::zero(),
705        }
706    }
707
708    /// Require at least `min_confidence` before a pre-computed update is used.
709    fn set_min_confidence(&mut self, min_confidence: A) {
710        self.min_confidence = min_confidence;
711    }
712
713    /// Serve a pre-computed update only when it was computed for a gradient
714    /// that matches the one that actually arrived.
715    fn try_get_precomputed(
716        &mut self,
717        actual_gradient: &Array1<A>,
718        tolerance: f64,
719    ) -> Option<PrecomputedUpdate<A>> {
720        // Remove expired updates
721        let now = Instant::now();
722        while let Some(update) = self.precomputed_updates.front() {
723            if update.valid_until <= now {
724                self.precomputed_updates.pop_front();
725            } else {
726                break;
727            }
728        }
729
730        let candidate = self.precomputed_updates.pop_front();
731        self.gradient_predictor.observe(actual_gradient);
732
733        match candidate {
734            Some(candidate)
735                if candidate.confidence >= self.min_confidence
736                    && gradient_matches(&candidate.gradient, actual_gradient, tolerance) =>
737            {
738                self.hits += 1;
739                Some(candidate)
740            }
741            _ => {
742                self.misses += 1;
743                None
744            }
745        }
746    }
747
748    /// Predict the next gradient and pre-compute the corresponding first-order
749    /// update.
750    ///
751    /// The stored update is a first-order (`params - lr * predicted_gradient`)
752    /// approximation of the wrapped optimizer's step, which is why it is only
753    /// ever served when the predicted gradient turns out to match the real one
754    /// within the configured tolerance.
755    fn start_precomputation(
756        &mut self,
757        _observed_gradient: &Array1<A>,
758        current_params: &Array1<A>,
759        learning_rate: A,
760        validity: Duration,
761    ) {
762        let Some((predicted, confidence)) = self.gradient_predictor.predict() else {
763            return;
764        };
765        if predicted.len() != current_params.len() {
766            return;
767        }
768
769        let mut update = current_params.clone();
770        for (p, &g) in update.iter_mut().zip(predicted.iter()) {
771            *p = *p - learning_rate * g;
772        }
773
774        if self.precomputed_updates.len() >= self.max_buffer_size {
775            self.precomputed_updates.pop_front();
776        }
777        self.precomputed_updates.push_back(PrecomputedUpdate {
778            gradient: predicted,
779            update,
780            valid_until: Instant::now() + validity,
781            confidence,
782        });
783    }
784
785    fn attempts(&self) -> usize {
786        self.hits + self.misses
787    }
788
789    /// Measured hit rate, or `None` when no step has consulted the engine yet.
790    fn hit_rate(&self) -> Option<f64> {
791        let attempts = self.attempts();
792        if attempts == 0 {
793            None
794        } else {
795            Some(self.hits as f64 / attempts as f64)
796        }
797    }
798}
799
800/// Relative agreement test used to decide whether a speculative update is
801/// still valid for the gradient that arrived.
802fn gradient_matches<A: Float>(predicted: &Array1<A>, actual: &Array1<A>, tolerance: f64) -> bool {
803    if predicted.len() != actual.len() || predicted.is_empty() {
804        return false;
805    }
806    let mut diff_sq = A::zero();
807    let mut actual_sq = A::zero();
808    for (&p, &a) in predicted.iter().zip(actual.iter()) {
809        let d = p - a;
810        diff_sq = diff_sq + d * d;
811        actual_sq = actual_sq + a * a;
812    }
813    let diff = diff_sq.sqrt().to_f64().unwrap_or(f64::INFINITY);
814    let scale = actual_sq.sqrt().to_f64().unwrap_or(0.0);
815    if !diff.is_finite() {
816        return false;
817    }
818    if scale <= f64::EPSILON {
819        diff <= tolerance
820    } else {
821        diff / scale <= tolerance
822    }
823}
824
825impl<A: Float + Send + Sync> LockFreeBuffer<A> {
826    fn new(capacity: usize) -> Self {
827        let capacity = capacity.max(1);
828        Self {
829            buffer: vec![None; capacity],
830            write_index: AtomicUsize::new(0),
831            read_index: AtomicUsize::new(0),
832            capacity,
833        }
834    }
835
836    /// Stage an update, dropping the oldest entry when the ring is full.
837    fn push(&mut self, value: Array1<A>) {
838        let write = self.write_index.load(Ordering::Relaxed);
839        let read = self.read_index.load(Ordering::Relaxed);
840        if write - read >= self.capacity {
841            // Ring is full: advance the reader, discarding the oldest entry.
842            let slot = read % self.capacity;
843            self.buffer[slot] = None;
844            self.read_index.store(read + 1, Ordering::Relaxed);
845        }
846        let slot = write % self.capacity;
847        self.buffer[slot] = Some(value);
848        self.write_index.store(write + 1, Ordering::Relaxed);
849    }
850
851    fn pop(&mut self) -> Option<Array1<A>> {
852        let read = self.read_index.load(Ordering::Relaxed);
853        if read == self.write_index.load(Ordering::Relaxed) {
854            return None;
855        }
856        let slot = read % self.capacity;
857        let value = self.buffer[slot].take();
858        self.read_index.store(read + 1, Ordering::Relaxed);
859        value
860    }
861
862    fn len(&self) -> usize {
863        self.write_index.load(Ordering::Relaxed) - self.read_index.load(Ordering::Relaxed)
864    }
865}
866
867impl<A: Float> FastMemoryPool<A> {
868    fn new(_total_size: usize, block_size_bytes: usize) -> Result<Self> {
869        let element_size = std::mem::size_of::<A>().max(1);
870        let elements_per_block = (block_size_bytes / element_size).max(1);
871        let total_blocks = _total_size / block_size_bytes.max(1);
872
873        let mut free_blocks = Vec::with_capacity(total_blocks);
874        for _ in 0..total_blocks {
875            free_blocks.push(Vec::with_capacity(elements_per_block));
876        }
877
878        Ok(Self {
879            free_blocks: Mutex::new(free_blocks),
880            elements_per_block,
881            total_blocks,
882            checked_out: AtomicUsize::new(0),
883            peak_checked_out: AtomicUsize::new(0),
884            misses: AtomicUsize::new(0),
885        })
886    }
887
888    /// Check out a pre-allocated scratch buffer able to hold `len` elements.
889    fn acquire(&self, len: usize) -> Option<Vec<A>> {
890        if len > self.elements_per_block {
891            self.misses.fetch_add(1, Ordering::Relaxed);
892            return None;
893        }
894        let block = lock_recovered(&self.free_blocks).pop();
895        match block {
896            Some(mut block) => {
897                block.clear();
898                let in_use = self.checked_out.fetch_add(1, Ordering::Relaxed) + 1;
899                self.peak_checked_out.fetch_max(in_use, Ordering::Relaxed);
900                Some(block)
901            }
902            None => {
903                self.misses.fetch_add(1, Ordering::Relaxed);
904                None
905            }
906        }
907    }
908
909    /// Return a buffer previously obtained from [`Self::acquire`].
910    fn release(&self, mut block: Vec<A>) {
911        if block.capacity() < self.elements_per_block {
912            // Not one of ours (the caller allocated it) — just drop it.
913            return;
914        }
915        block.clear();
916        let mut free = lock_recovered(&self.free_blocks);
917        if free.len() < self.total_blocks {
918            free.push(block);
919            drop(free);
920            let previous = self.checked_out.load(Ordering::Relaxed);
921            if previous > 0 {
922                self.checked_out.store(previous - 1, Ordering::Relaxed);
923            }
924        }
925    }
926
927    /// Fraction of the pool that has actually been exercised (high-water mark
928    /// of simultaneously checked-out blocks). Returns `0.0` for an empty pool
929    /// instead of dividing by zero.
930    fn get_efficiency(&self) -> f64 {
931        if self.total_blocks == 0 {
932            return 0.0;
933        }
934        self.peak_checked_out.load(Ordering::Relaxed) as f64 / self.total_blocks as f64
935    }
936
937    fn misses(&self) -> usize {
938        self.misses.load(Ordering::Relaxed)
939    }
940}
941
942impl<A: Float + Send + Sync> SIMDProcessor<A> {
943    fn new(enabled: bool, batch_threshold: usize) -> Self {
944        Self {
945            enabled,
946            vector_width: batch_threshold.max(1),
947            _element: std::marker::PhantomData,
948        }
949    }
950
951    /// The chunked path only pays for itself once there is at least one full
952    /// chunk of work, which is exactly what `batch_threshold` configures.
953    fn is_active(&self, len: usize) -> bool {
954        self.enabled && len >= self.vector_width
955    }
956
957    /// `params -= learning_rate * gradient`, walked in contiguous chunks so
958    /// the inner loop is a fixed-width, auto-vectorizable kernel.
959    fn apply_scaled_subtract(
960        &self,
961        params: &mut Array1<A>,
962        gradient: &Array1<A>,
963        learning_rate: A,
964    ) {
965        let width = self.vector_width.max(1);
966        match (params.as_slice_mut(), gradient.as_slice()) {
967            (Some(p), Some(g)) => {
968                for (p_chunk, g_chunk) in p.chunks_mut(width).zip(g.chunks(width)) {
969                    for (p_value, g_value) in p_chunk.iter_mut().zip(g_chunk.iter()) {
970                        *p_value = *p_value - learning_rate * *g_value;
971                    }
972                }
973            }
974            _ => {
975                for (p_value, g_value) in params.iter_mut().zip(gradient.iter()) {
976                    *p_value = *p_value - learning_rate * *g_value;
977                }
978            }
979        }
980    }
981}
982
983impl<A: Float + Send + Sync> GradientQuantizer<A> {
984    fn new(bits: u8) -> Self {
985        Self {
986            // 1..=24 keeps `1 << (bits - 1)` well inside `u32` and keeps the
987            // level count non-zero, so the scale can never become 0.
988            bits: bits.clamp(1, 24),
989            scale: A::one(),
990            zero_point: A::zero(),
991            error_accumulator: None,
992        }
993    }
994
995    /// Symmetric linear quantization with error feedback.
996    ///
997    /// L4: the previous version computed `scale = max_abs / levels` and then
998    /// divided by it unconditionally. For an all-zero gradient (a normal
999    /// occurrence once a stream converges, and the default state of a freshly
1000    /// initialised model) `max_abs` is 0, so every element became `0/0 = NaN`
1001    /// and the NaN propagated into the parameters. `bits = 0` produced the
1002    /// same division by zero via `2^0 - 1 = 0`.
1003    fn quantize(&mut self, gradient: &Array1<A>) -> Result<Array1<A>> {
1004        let n = gradient.len();
1005        if n == 0 {
1006            return Ok(gradient.clone());
1007        }
1008
1009        // Error feedback: carry the previous step's rounding residual forward
1010        // so quantization does not introduce a systematic bias.
1011        let compensated = match self.error_accumulator.as_ref() {
1012            Some(error) if error.len() == n => gradient + error,
1013            _ => gradient.clone(),
1014        };
1015
1016        // NaN loses every `>` comparison, so a fold-based maximum silently
1017        // ignores it; the window has to be scanned for finiteness explicitly.
1018        if compensated.iter().any(|value| !value.is_finite()) {
1019            return Err(OptimError::InvalidParameter(
1020                "cannot quantize a gradient containing non-finite values".to_string(),
1021            ));
1022        }
1023        let max_abs = compensated.iter().fold(
1024            A::zero(),
1025            |acc, x| if x.abs() > acc { x.abs() } else { acc },
1026        );
1027
1028        self.zero_point = A::zero(); // symmetric quantization
1029        if max_abs == A::zero() {
1030            // Nothing to quantize; the representation is exact.
1031            self.scale = A::one();
1032            self.error_accumulator = Some(Array1::zeros(n));
1033            return Ok(compensated);
1034        }
1035
1036        let level_count = (1u32 << (self.bits.max(1) as u32 - 1))
1037            .saturating_sub(1)
1038            .max(1);
1039        let levels = A::from(level_count).unwrap_or(A::one());
1040        self.scale = max_abs / levels;
1041        let scale = self.scale;
1042        let zero_point = self.zero_point;
1043
1044        let quantized = compensated.mapv(|x| {
1045            let mut q = (x / scale).round();
1046            if q > levels {
1047                q = levels;
1048            } else if q < -levels {
1049                q = -levels;
1050            }
1051            q * scale + zero_point
1052        });
1053
1054        self.error_accumulator = Some(&compensated - &quantized);
1055        Ok(quantized)
1056    }
1057}
1058
1059impl LatencyMonitor {
1060    fn new(maxsamples: usize) -> Self {
1061        Self {
1062            latency_samples: VecDeque::with_capacity(maxsamples),
1063            maxsamples: maxsamples.max(1),
1064            p50_latency: Duration::from_micros(0),
1065            p95_latency: Duration::from_micros(0),
1066            p99_latency: Duration::from_micros(0),
1067            violations: 0,
1068            total_operations: 0,
1069        }
1070    }
1071
1072    fn record_latency(&mut self, latency: Duration) {
1073        self.latency_samples.push_back(latency);
1074        if self.latency_samples.len() > self.maxsamples {
1075            self.latency_samples.pop_front();
1076        }
1077
1078        self.total_operations += 1;
1079        self.update_percentiles();
1080    }
1081
1082    fn update_percentiles(&mut self) {
1083        if self.latency_samples.is_empty() {
1084            return;
1085        }
1086
1087        let mut sorted: Vec<_> = self.latency_samples.iter().cloned().collect();
1088        sorted.sort();
1089
1090        let last = sorted.len() - 1;
1091        let index_for = |q: f64| ((sorted.len() as f64 * q) as usize).min(last);
1092        self.p50_latency = sorted[index_for(0.50)];
1093        self.p95_latency = sorted[index_for(0.95)];
1094        self.p99_latency = sorted[index_for(0.99)];
1095    }
1096
1097    fn get_average_latency(&self) -> Duration {
1098        if self.latency_samples.is_empty() {
1099            Duration::from_micros(0)
1100        } else {
1101            let total: Duration = self.latency_samples.iter().sum();
1102            total / self.latency_samples.len() as u32
1103        }
1104    }
1105}
1106
1107impl<A: Float + Send + Sync> ApproximationController<A> {
1108    fn new(targetlatency: Duration) -> Self {
1109        Self {
1110            approximation_level: A::zero(),
1111            performance_history: VecDeque::with_capacity(100),
1112            adaptation_rate: A::from(0.1).unwrap_or_else(A::one),
1113            targetlatency,
1114        }
1115    }
1116
1117    fn get_approximation_level(&self) -> A {
1118        self.approximation_level
1119    }
1120
1121    fn record_performance(&mut self, latency: Duration, _approximation_level: A, accuracy: A) {
1122        let now = Instant::now();
1123        let point = PerformancePoint {
1124            latency,
1125            accuracy,
1126            timestamp: now,
1127        };
1128
1129        self.performance_history.push_back(point);
1130        // Bound the window by age as well as by count: a controller that reacts
1131        // to latencies measured minutes ago is chasing a workload that no
1132        // longer exists. `timestamp` was recorded for exactly this and never
1133        // read.
1134        while self
1135            .performance_history
1136            .front()
1137            .is_some_and(|p| now.duration_since(p.timestamp) > PERFORMANCE_WINDOW_AGE)
1138        {
1139            self.performance_history.pop_front();
1140        }
1141        if self.performance_history.len() > PERFORMANCE_WINDOW_LEN {
1142            self.performance_history.pop_front();
1143        }
1144
1145        self.adapt_approximation_level();
1146    }
1147
1148    /// Mean latency over the retained window, or `None` when it is empty.
1149    fn mean_latency(&self) -> Option<Duration> {
1150        let count = self.performance_history.len();
1151        if count == 0 {
1152            return None;
1153        }
1154        let total: Duration = self.performance_history.iter().map(|p| p.latency).sum();
1155        Some(total / count as u32)
1156    }
1157
1158    /// Move the approximation level towards the latency target.
1159    ///
1160    /// Driven by the *mean* latency of the retained window rather than the
1161    /// single latest sample: every latency was already being recorded but only
1162    /// the newest one was ever looked at, so one unlucky slow step swung the
1163    /// approximation level as hard as a sustained regression.
1164    fn adapt_approximation_level(&mut self) {
1165        let Some(latency) = self.mean_latency() else {
1166            return;
1167        };
1168        let target = self.targetlatency.as_micros().max(1) as f64;
1169        let latency_ratio = latency.as_micros() as f64 / target;
1170
1171        if latency_ratio > 1.1 {
1172            // Latency too high, increase approximation
1173            self.approximation_level =
1174                (self.approximation_level + self.adaptation_rate).min(A::one());
1175        } else if latency_ratio < 0.8 {
1176            // Latency low, can reduce approximation
1177            self.approximation_level =
1178                (self.approximation_level - self.adaptation_rate).max(A::zero());
1179        }
1180    }
1181
1182    fn increase_approximation(&mut self) {
1183        let double = A::from(2.0).unwrap_or_else(A::one);
1184        self.approximation_level =
1185            (self.approximation_level + self.adaptation_rate * double).min(A::one());
1186    }
1187
1188    /// Mean accuracy observed over the retained performance window.
1189    fn mean_accuracy(&self) -> Option<A> {
1190        if self.performance_history.is_empty() {
1191            return None;
1192        }
1193        let count = A::from(self.performance_history.len())?;
1194        let sum = self
1195            .performance_history
1196            .iter()
1197            .fold(A::zero(), |acc, point| acc + point.accuracy);
1198        Some(sum / count)
1199    }
1200}
1201
1202impl<A: Float + Send + Sync + std::iter::Sum> GradientPredictor<A> {
1203    fn new(windowsize: usize) -> Self {
1204        Self {
1205            gradient_history: VecDeque::with_capacity(windowsize.max(2)),
1206            trend_weights: None,
1207            windowsize: windowsize.max(2),
1208            confidence: None,
1209            pending_prediction: None,
1210        }
1211    }
1212
1213    /// Record the gradient that actually arrived and score the outstanding
1214    /// prediction against it.
1215    fn observe(&mut self, gradient: &Array1<A>) {
1216        if let Some(prediction) = self.pending_prediction.take() {
1217            if prediction.len() == gradient.len() {
1218                let similarity = cosine_similarity(&prediction, gradient);
1219                let alpha = A::from(0.2).unwrap_or_else(A::one);
1220                self.confidence = Some(match self.confidence {
1221                    Some(previous) => previous * (A::one() - alpha) + similarity * alpha,
1222                    None => similarity,
1223                });
1224            }
1225        }
1226
1227        self.gradient_history.push_back(gradient.clone());
1228        while self.gradient_history.len() > self.windowsize {
1229            self.gradient_history.pop_front();
1230        }
1231        self.recompute_trend();
1232    }
1233
1234    /// Per-coordinate ordinary-least-squares slope over the retained window.
1235    fn recompute_trend(&mut self) {
1236        let n = self.gradient_history.len();
1237        if n < 2 {
1238            self.trend_weights = None;
1239            return;
1240        }
1241        let dim = match self.gradient_history.back() {
1242            Some(last) => last.len(),
1243            None => return,
1244        };
1245        if self.gradient_history.iter().any(|g| g.len() != dim) {
1246            self.trend_weights = None;
1247            return;
1248        }
1249
1250        // x = 0..n-1, so sum(x) and sum((x - x_mean)^2) are closed forms.
1251        let n_f = A::from(n).unwrap_or_else(A::one);
1252        let x_mean = A::from((n - 1) as f64 / 2.0).unwrap_or_else(A::zero);
1253        let mut denominator = A::zero();
1254        for i in 0..n {
1255            let dx = A::from(i).unwrap_or_else(A::zero) - x_mean;
1256            denominator = denominator + dx * dx;
1257        }
1258        if denominator == A::zero() {
1259            self.trend_weights = None;
1260            return;
1261        }
1262
1263        let mut slopes = Array1::zeros(dim);
1264        for coordinate in 0..dim {
1265            let mut y_sum = A::zero();
1266            for gradient in &self.gradient_history {
1267                y_sum = y_sum + gradient[coordinate];
1268            }
1269            let y_mean = y_sum / n_f;
1270            let mut numerator = A::zero();
1271            for (i, gradient) in self.gradient_history.iter().enumerate() {
1272                let dx = A::from(i).unwrap_or_else(A::zero) - x_mean;
1273                numerator = numerator + dx * (gradient[coordinate] - y_mean);
1274            }
1275            slopes[coordinate] = numerator / denominator;
1276        }
1277        self.trend_weights = Some(slopes);
1278    }
1279
1280    /// Linear extrapolation of the next gradient, with the measured
1281    /// confidence of the previous prediction.
1282    fn predict(&mut self) -> Option<(Array1<A>, A)> {
1283        let last = self.gradient_history.back()?.clone();
1284        let slopes = self.trend_weights.as_ref()?;
1285        if slopes.len() != last.len() {
1286            return None;
1287        }
1288        let mut predicted = last;
1289        for (value, &slope) in predicted.iter_mut().zip(slopes.iter()) {
1290            *value = *value + slope;
1291        }
1292        self.pending_prediction = Some(predicted.clone());
1293        // Until a prediction has been scored there is no measured confidence;
1294        // report zero rather than inventing one.
1295        let confidence = self.confidence.unwrap_or_else(A::zero);
1296        Some((predicted, confidence))
1297    }
1298}
1299
1300/// Cosine similarity between two equally sized vectors, `0` when either is
1301/// degenerate.
1302fn cosine_similarity<A: Float>(a: &Array1<A>, b: &Array1<A>) -> A {
1303    if a.len() != b.len() {
1304        return A::zero();
1305    }
1306    let mut dot = A::zero();
1307    let mut norm_a = A::zero();
1308    let mut norm_b = A::zero();
1309    for (&x, &y) in a.iter().zip(b.iter()) {
1310        dot = dot + x * y;
1311        norm_a = norm_a + x * x;
1312        norm_b = norm_b + y * y;
1313    }
1314    let norm_a = norm_a.sqrt();
1315    let norm_b = norm_b.sqrt();
1316    if norm_a == A::zero() || norm_b == A::zero() {
1317        A::zero()
1318    } else {
1319        dot / (norm_a * norm_b)
1320    }
1321}
1322
1323/// Performance metrics for low-latency optimization
1324#[derive(Debug, Clone)]
1325pub struct LowLatencyMetrics {
1326    /// Average latency (microseconds)
1327    pub avg_latency_us: u64,
1328    /// Median latency (microseconds)
1329    pub p50_latency_us: u64,
1330    /// 95th percentile latency (microseconds)
1331    pub p95_latency_us: u64,
1332    /// 99th percentile latency (microseconds)
1333    pub p99_latency_us: u64,
1334    /// Number of latency violations
1335    pub latency_violations: usize,
1336    /// Total operations performed
1337    pub total_operations: usize,
1338    /// Current approximation level (0.0 to 1.0)
1339    pub current_approximation_level: f64,
1340    /// Mean cosine agreement between the applied step and the descent
1341    /// direction over the retained window, or `None` before the first step.
1342    pub approximation_accuracy: Option<f64>,
1343    /// Measured pre-computation hit rate, or `None` when pre-computation is
1344    /// disabled or has not been consulted yet.
1345    pub precomputation_hit_rate: Option<f64>,
1346    /// Number of steps that consulted the pre-computation engine
1347    pub precomputation_attempts: usize,
1348    /// Fraction of the memory pool that has been exercised
1349    pub memory_efficiency: f64,
1350    /// Scratch requests the memory pool could not satisfy
1351    pub memory_pool_misses: usize,
1352}
1353
1354#[cfg(test)]
1355mod tests {
1356    use super::*;
1357    use crate::optimizers::SGD;
1358
1359    #[test]
1360    fn test_low_latency_config() {
1361        let config = LowLatencyConfig::default();
1362        assert_eq!(config.target_latency_us, 100);
1363        assert!(config.enable_precomputation);
1364        assert!(config.enable_lock_free);
1365    }
1366
1367    #[test]
1368    fn test_low_latency_optimizer_creation() {
1369        let sgd = SGD::new(0.01f64);
1370        let config = LowLatencyConfig::default();
1371        let result = LowLatencyOptimizer::new(sgd, config);
1372        assert!(result.is_ok());
1373    }
1374
1375    #[test]
1376    fn test_latency_monitor() {
1377        let mut monitor = LatencyMonitor::new(10);
1378
1379        for i in 1..=5 {
1380            monitor.record_latency(Duration::from_micros(i * 100));
1381        }
1382
1383        assert_eq!(monitor.total_operations, 5);
1384        assert!(monitor.get_average_latency().as_micros() > 0);
1385    }
1386
1387    #[test]
1388    fn test_gradient_quantizer() {
1389        let mut quantizer = GradientQuantizer::new(8);
1390        let gradient = Array1::from_vec(vec![0.1f64, 0.5, -0.3, 0.8]);
1391
1392        let result = quantizer.quantize(&gradient);
1393        assert!(result.is_ok());
1394
1395        let quantized = result.expect("quantization of a finite gradient must succeed");
1396        assert_eq!(quantized.len(), gradient.len());
1397    }
1398
1399    #[test]
1400    fn test_approximation_controller() {
1401        let mut controller = ApproximationController::new(Duration::from_micros(100));
1402
1403        // Record high latency - should increase approximation
1404        controller.record_performance(Duration::from_micros(200), 0.0f64, 0.9f64);
1405
1406        assert!(controller.get_approximation_level() > 0.0);
1407    }
1408
1409    #[test]
1410    fn test_lock_free_buffer() {
1411        let buffer = LockFreeBuffer::<f64>::new(4);
1412        assert_eq!(buffer.capacity, 4);
1413        assert_eq!(buffer.write_index.load(Ordering::Relaxed), 0);
1414        assert_eq!(buffer.read_index.load(Ordering::Relaxed), 0);
1415    }
1416}