feat(ml): add Thompson Sampling and Bayesian promotion to A/B testing

Extends TrafficSplitter with Thompson Sampling (Beta-distributed traffic
allocation) and PromotionCriteria for automated champion/challenger
decisions. 18 tests, 0 clippy.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-03-03 05:09:12 +01:00
parent 845f8707b9
commit 30dbce211a
4 changed files with 613 additions and 55 deletions

View File

@@ -8,13 +8,11 @@ use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use tokio::sync::{RwLock, Mutex};
use tokio::sync::Mutex;
use uuid::Uuid;
use crate::{MLError, MLResult, ModelType, Features, ModelPrediction, MLModel};
use super::{ModelVersion, DeploymentStatus};
use crate::{MLError, MLResult, Features, ModelPrediction, MLModel};
/// A/B test configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -50,7 +48,7 @@ impl Default for ABTestConfig {
treatment_percentage: 0.5,
min_sample_size: 1000,
significance_threshold: 0.05,
max_duration: Duration::from_hours(24),
max_duration: Duration::from_secs(24 * 60 * 60),
tracked_metrics: vec!["latency".to_owned(), "accuracy".to_owned(), "error_rate".to_owned()],
early_stopping: Some(EarlyStoppingConfig::default()),
splitting_strategy: TrafficSplittingStrategy::HashBased,
@@ -74,7 +72,7 @@ pub struct EarlyStoppingConfig {
impl Default for EarlyStoppingConfig {
fn default() -> Self {
Self {
check_interval: Duration::from_minutes(5),
check_interval: Duration::from_secs(5 * 60),
min_samples_for_early_stop: 100,
max_degradation_threshold: 0.1, // 10% degradation
statistical_power_threshold: 0.8, // 80% power
@@ -93,6 +91,8 @@ pub enum TrafficSplittingStrategy {
RoundRobin,
/// Weighted random splitting
WeightedRandom,
/// Thompson Sampling — Bayesian adaptive traffic allocation
ThompsonSampling,
}
/// A/B test status
@@ -202,7 +202,7 @@ impl ABTestExperiment {
// Determine which model to use
let group = self.traffic_splitter.assign_group(features);
let (model, group_name) = match group {
let (model, _group_name) = match group {
TestGroup::Control => (&self.control_model, "control"),
TestGroup::Treatment => (&self.treatment_model, "treatment"),
};
@@ -411,7 +411,260 @@ pub enum TestGroup {
Treatment,
}
/// Thompson Sampling state for Bayesian adaptive traffic splitting.
///
/// Maintains Beta distribution parameters for both champion (control) and
/// challenger (treatment) models. The champion starts with a strong prior
/// Beta(10,1) reflecting production confidence; the challenger starts with
/// an uninformative Beta(1,1).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThompsonSamplingState {
/// Beta distribution alpha (successes + prior) for the champion model
pub control_alpha: f64,
/// Beta distribution beta (failures + prior) for the champion model
pub control_beta: f64,
/// Beta distribution alpha (successes + prior) for the challenger model
pub treatment_alpha: f64,
/// Beta distribution beta (failures + prior) for the challenger model
pub treatment_beta: f64,
/// Total trades observed for control
pub control_trades: usize,
/// Total trades observed for treatment
pub treatment_trades: usize,
}
impl Default for ThompsonSamplingState {
fn default() -> Self {
Self::new()
}
}
impl ThompsonSamplingState {
/// Create a new Thompson Sampling state with default priors.
///
/// Champion: Beta(10, 1) — strong prior reflecting production confidence.
/// Challenger: Beta(1, 1) — uninformative prior.
pub fn new() -> Self {
Self {
control_alpha: 10.0,
control_beta: 1.0,
treatment_alpha: 1.0,
treatment_beta: 1.0,
control_trades: 0,
treatment_trades: 0,
}
}
/// Sample from both Beta distributions and return the group whose
/// sample is higher. This is the core Thompson Sampling decision:
/// models that are believed to be better get more traffic, but
/// exploration is preserved via randomness.
///
/// Uses the Gamma-distribution trick: Beta(a,b) = X/(X+Y) where
/// X ~ Gamma(a,1), Y ~ Gamma(b,1).
pub fn sample_and_assign(&self) -> TestGroup {
let control_sample = self.sample_beta(self.control_alpha, self.control_beta);
let treatment_sample = self.sample_beta(self.treatment_alpha, self.treatment_beta);
if control_sample >= treatment_sample {
TestGroup::Control
} else {
TestGroup::Treatment
}
}
/// Record the outcome of a trade for a given group.
///
/// On success: alpha += 1 (evidence of reward).
/// On failure: beta += 1 (evidence of non-reward).
pub fn record_outcome(&mut self, group: TestGroup, success: bool) {
match group {
TestGroup::Control => {
self.control_trades += 1;
if success {
self.control_alpha += 1.0;
} else {
self.control_beta += 1.0;
}
}
TestGroup::Treatment => {
self.treatment_trades += 1;
if success {
self.treatment_alpha += 1.0;
} else {
self.treatment_beta += 1.0;
}
}
}
}
/// Sample from a Beta(alpha, beta) distribution using the Gamma trick.
///
/// Beta(a,b) = X/(X+Y) where X ~ Gamma(a,1) and Y ~ Gamma(b,1).
fn sample_beta(&self, alpha: f64, beta: f64) -> f64 {
use rand::Rng;
use rand_distr::{Distribution, Gamma};
let mut rng = rand::thread_rng();
// Alpha and beta must be > 0 for Gamma. Clamp to epsilon.
let safe_alpha = alpha.max(1e-10);
let safe_beta = beta.max(1e-10);
let gamma_a = match Gamma::new(safe_alpha, 1.0) {
Ok(d) => d.sample(&mut rng),
Err(_) => rng.gen::<f64>(), // fallback: uniform [0,1)
};
let gamma_b = match Gamma::new(safe_beta, 1.0) {
Ok(d) => d.sample(&mut rng),
Err(_) => rng.gen::<f64>(),
};
let sum = gamma_a + gamma_b;
if sum <= 0.0 {
0.5 // degenerate case — return neutral
} else {
gamma_a / sum
}
}
/// Estimate P(treatment > control) via Monte Carlo sampling.
///
/// Draws `n_samples` paired samples from both Beta distributions and
/// returns the fraction where treatment sample exceeds control sample.
pub fn estimate_probability_treatment_better(&self, n_samples: usize) -> f64 {
if n_samples == 0 {
return 0.0;
}
let mut treatment_wins = 0_usize;
for _ in 0..n_samples {
let ctrl = self.sample_beta(self.control_alpha, self.control_beta);
let treat = self.sample_beta(self.treatment_alpha, self.treatment_beta);
if treat > ctrl {
treatment_wins += 1;
}
}
treatment_wins as f64 / n_samples as f64
}
}
/// Decision returned by promotion criteria evaluation.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum PromotionDecision {
/// Not enough evidence yet to promote
NotReady {
/// Explanation of why promotion is not ready
reason: String,
},
/// All statistical criteria met; awaiting manual approval
ReadyForApproval,
/// All criteria met and manual approval not required — auto-promoted
AutoPromoted,
}
/// Criteria for promoting a challenger model to champion.
///
/// All conditions must be satisfied simultaneously:
/// 1. Minimum trade count on the challenger
/// 2. Bayesian confidence threshold (P(challenger > champion) > threshold)
/// 3. Maximum drawdown ratio constraint
/// 4. Optional manual approval gate
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PromotionCriteria {
/// Minimum number of trades the challenger must have observed
pub min_trades: usize,
/// Required P(challenger > champion) for promotion (e.g. 0.95)
pub bayesian_confidence: f64,
/// Maximum allowed ratio of challenger DD to champion DD
pub max_drawdown_ratio: f64,
/// Whether a human must manually approve after criteria are met
pub require_manual_approval: bool,
}
impl Default for PromotionCriteria {
fn default() -> Self {
Self {
min_trades: 500,
bayesian_confidence: 0.95,
max_drawdown_ratio: 1.2,
require_manual_approval: true,
}
}
}
impl PromotionCriteria {
/// Evaluate whether the challenger should be promoted.
///
/// Returns `NotReady` with a reason if any criterion fails,
/// `ReadyForApproval` if manual approval is required, or
/// `AutoPromoted` if all criteria pass and no approval needed.
///
/// `n_mc_samples` controls Monte Carlo accuracy for Bayesian confidence
/// (default: 10_000).
pub fn should_promote(
&self,
state: &ThompsonSamplingState,
challenger_dd: f64,
champion_dd: f64,
) -> PromotionDecision {
self.should_promote_with_samples(state, challenger_dd, champion_dd, 10_000)
}
/// Same as `should_promote` but with configurable MC sample count
/// (useful for deterministic testing).
pub fn should_promote_with_samples(
&self,
state: &ThompsonSamplingState,
challenger_dd: f64,
champion_dd: f64,
n_mc_samples: usize,
) -> PromotionDecision {
// 1. Check minimum trades
if state.treatment_trades < self.min_trades {
return PromotionDecision::NotReady {
reason: format!(
"Insufficient trades: {} / {} required",
state.treatment_trades, self.min_trades
),
};
}
// 2. Bayesian confidence: P(challenger > champion)
let p_better = state.estimate_probability_treatment_better(n_mc_samples);
if p_better < self.bayesian_confidence {
return PromotionDecision::NotReady {
reason: format!(
"Bayesian confidence too low: {:.4} < {:.4}",
p_better, self.bayesian_confidence
),
};
}
// 3. Drawdown constraint: challenger DD must not exceed ratio * champion DD
// Guard against champion_dd == 0: any positive challenger_dd would fail.
let dd_limit = champion_dd * self.max_drawdown_ratio;
if challenger_dd > dd_limit {
return PromotionDecision::NotReady {
reason: format!(
"Challenger drawdown {:.4} exceeds {:.1}x champion drawdown {:.4} (limit {:.4})",
challenger_dd, self.max_drawdown_ratio, champion_dd, dd_limit
),
};
}
// All criteria met
if self.require_manual_approval {
PromotionDecision::ReadyForApproval
} else {
PromotionDecision::AutoPromoted
}
}
}
/// Traffic splitter for A/B testing
#[derive(Debug)]
pub struct TrafficSplitter {
/// Control group percentage
control_percentage: f32,
@@ -421,6 +674,8 @@ pub struct TrafficSplitter {
strategy: TrafficSplittingStrategy,
/// Round-robin counter
round_robin_counter: AtomicU64,
/// Thompson Sampling state (only present when strategy == ThompsonSampling)
thompson_state: Option<ThompsonSamplingState>,
}
impl TrafficSplitter {
@@ -430,14 +685,29 @@ impl TrafficSplitter {
treatment_percentage: f32,
strategy: TrafficSplittingStrategy,
) -> Self {
let thompson_state =
(strategy == TrafficSplittingStrategy::ThompsonSampling)
.then(ThompsonSamplingState::new);
Self {
control_percentage,
treatment_percentage,
strategy,
round_robin_counter: AtomicU64::new(0),
thompson_state,
}
}
/// Get a reference to the Thompson Sampling state (if active).
pub fn thompson_state(&self) -> Option<&ThompsonSamplingState> {
self.thompson_state.as_ref()
}
/// Get a mutable reference to the Thompson Sampling state (if active).
pub fn thompson_state_mut(&mut self) -> Option<&mut ThompsonSamplingState> {
self.thompson_state.as_mut()
}
/// Assign test group for given features
pub fn assign_group(&self, features: &Features) -> TestGroup {
match self.strategy {
@@ -445,6 +715,14 @@ impl TrafficSplitter {
TrafficSplittingStrategy::Random => self.random_assignment(),
TrafficSplittingStrategy::RoundRobin => self.round_robin_assignment(),
TrafficSplittingStrategy::WeightedRandom => self.weighted_random_assignment(),
TrafficSplittingStrategy::ThompsonSampling => {
if let Some(ref state) = self.thompson_state {
state.sample_and_assign()
} else {
// Fallback to random if state is somehow missing
self.random_assignment()
}
}
}
}
@@ -518,6 +796,7 @@ pub struct ABTestPrediction {
}
/// Metrics collector for A/B tests
#[derive(Debug)]
pub struct ABTestMetricsCollector {
/// Experiment ID
experiment_id: Uuid,
@@ -626,11 +905,8 @@ pub struct GroupMetricsSummary {
impl GroupMetricsSummary {
/// Create summary from group metrics
fn from_group_metrics(metrics: &GroupMetrics) -> Self {
let error_rate = if metrics.sample_count > 0 {
Some(metrics.error_count as f64 / metrics.sample_count as f64)
} else {
None
};
let error_rate = (metrics.sample_count > 0)
.then(|| metrics.error_count as f64 / metrics.sample_count as f64);
let (avg_latency, latency_std_dev) = if !metrics.latencies.is_empty() {
let avg = metrics.latencies.iter().sum::<f64>() / metrics.latencies.len() as f64;
@@ -784,4 +1060,300 @@ mod tests {
let (lower, upper) = experiment.calculate_confidence_interval(100.0, 110.0, 10.0, 12.0, 50.0, 50.0);
assert!(lower < upper);
}
// ---------------------------------------------------------------
// Thompson Sampling tests
// ---------------------------------------------------------------
#[test]
fn test_thompson_sampling_state_new() {
let state = ThompsonSamplingState::new();
// Champion: strong prior Beta(10, 1)
assert!((state.control_alpha - 10.0).abs() < f64::EPSILON);
assert!((state.control_beta - 1.0).abs() < f64::EPSILON);
// Challenger: uninformative prior Beta(1, 1)
assert!((state.treatment_alpha - 1.0).abs() < f64::EPSILON);
assert!((state.treatment_beta - 1.0).abs() < f64::EPSILON);
assert_eq!(state.control_trades, 0);
assert_eq!(state.treatment_trades, 0);
}
#[test]
fn test_thompson_sampling_default_matches_new() {
let from_new = ThompsonSamplingState::new();
let from_default = ThompsonSamplingState::default();
assert!((from_new.control_alpha - from_default.control_alpha).abs() < f64::EPSILON);
assert!((from_new.treatment_alpha - from_default.treatment_alpha).abs() < f64::EPSILON);
}
#[test]
fn test_thompson_sampling_record_outcome() {
let mut state = ThompsonSamplingState::new();
let orig_ctrl_alpha = state.control_alpha;
let orig_ctrl_beta = state.control_beta;
let orig_treat_alpha = state.treatment_alpha;
let orig_treat_beta = state.treatment_beta;
// Control success: alpha should increase by 1
state.record_outcome(TestGroup::Control, true);
assert!((state.control_alpha - (orig_ctrl_alpha + 1.0)).abs() < f64::EPSILON);
assert!((state.control_beta - orig_ctrl_beta).abs() < f64::EPSILON);
assert_eq!(state.control_trades, 1);
// Control failure: beta should increase by 1
state.record_outcome(TestGroup::Control, false);
assert!((state.control_beta - (orig_ctrl_beta + 1.0)).abs() < f64::EPSILON);
assert_eq!(state.control_trades, 2);
// Treatment success
state.record_outcome(TestGroup::Treatment, true);
assert!((state.treatment_alpha - (orig_treat_alpha + 1.0)).abs() < f64::EPSILON);
assert!((state.treatment_beta - orig_treat_beta).abs() < f64::EPSILON);
assert_eq!(state.treatment_trades, 1);
// Treatment failure
state.record_outcome(TestGroup::Treatment, false);
assert!((state.treatment_beta - (orig_treat_beta + 1.0)).abs() < f64::EPSILON);
assert_eq!(state.treatment_trades, 2);
}
#[test]
fn test_thompson_sampling_returns_valid_group() {
let state = ThompsonSamplingState::new();
// Run many samples — each must return a valid group
for _ in 0..500 {
let group = state.sample_and_assign();
assert!(group == TestGroup::Control || group == TestGroup::Treatment);
}
}
#[test]
fn test_thompson_sampling_strong_prior() {
// Champion with overwhelming evidence Beta(100, 1) vs Challenger Beta(1, 1)
let state = ThompsonSamplingState {
control_alpha: 100.0,
control_beta: 1.0,
treatment_alpha: 1.0,
treatment_beta: 1.0,
control_trades: 0,
treatment_trades: 0,
};
let mut control_count = 0;
let iterations = 1000;
for _ in 0..iterations {
if state.sample_and_assign() == TestGroup::Control {
control_count += 1;
}
}
// With Beta(100,1) vs Beta(1,1), champion should win >80% of the time
let control_ratio = control_count as f64 / iterations as f64;
assert!(
control_ratio > 0.80,
"Champion with Beta(100,1) should dominate, but ratio was {control_ratio:.3}"
);
}
#[test]
fn test_thompson_sampling_estimate_probability() {
// Treatment is much better: Beta(50, 5) vs Control Beta(5, 50)
let state = ThompsonSamplingState {
control_alpha: 5.0,
control_beta: 50.0,
treatment_alpha: 50.0,
treatment_beta: 5.0,
control_trades: 55,
treatment_trades: 55,
};
let p = state.estimate_probability_treatment_better(5000);
// Treatment should be better with very high probability
assert!(
p > 0.95,
"P(treatment > control) should be > 0.95, got {p:.4}"
);
}
#[test]
fn test_promotion_criteria_defaults() {
let criteria = PromotionCriteria::default();
assert_eq!(criteria.min_trades, 500);
assert!((criteria.bayesian_confidence - 0.95).abs() < f64::EPSILON);
assert!((criteria.max_drawdown_ratio - 1.2).abs() < f64::EPSILON);
assert!(criteria.require_manual_approval);
}
#[test]
fn test_promotion_not_ready_insufficient_trades() {
let criteria = PromotionCriteria::default();
let state = ThompsonSamplingState {
control_alpha: 1.0,
control_beta: 1.0,
treatment_alpha: 100.0,
treatment_beta: 1.0,
control_trades: 1000,
treatment_trades: 499, // below min_trades (500)
};
let decision = criteria.should_promote(&state, 0.05, 0.10);
match decision {
PromotionDecision::NotReady { reason } => {
assert!(
reason.contains("Insufficient trades"),
"Expected insufficient trades reason, got: {reason}"
);
}
other => panic!("Expected NotReady, got {other:?}"),
}
}
#[test]
fn test_promotion_not_ready_low_confidence() {
let criteria = PromotionCriteria {
min_trades: 10,
bayesian_confidence: 0.95,
max_drawdown_ratio: 2.0,
require_manual_approval: false,
};
// Treatment is much worse: Beta(1, 50) — will fail confidence check
let state = ThompsonSamplingState {
control_alpha: 50.0,
control_beta: 1.0,
treatment_alpha: 1.0,
treatment_beta: 50.0,
control_trades: 51,
treatment_trades: 51,
};
let decision = criteria.should_promote_with_samples(&state, 0.01, 0.10, 5000);
match decision {
PromotionDecision::NotReady { reason } => {
assert!(
reason.contains("confidence"),
"Expected confidence reason, got: {reason}"
);
}
other => panic!("Expected NotReady due to low confidence, got {other:?}"),
}
}
#[test]
fn test_promotion_drawdown_check() {
let criteria = PromotionCriteria {
min_trades: 10,
bayesian_confidence: 0.01, // very low threshold so confidence always passes
max_drawdown_ratio: 1.2,
require_manual_approval: false,
};
// Treatment has good stats
let state = ThompsonSamplingState {
control_alpha: 10.0,
control_beta: 10.0,
treatment_alpha: 50.0,
treatment_beta: 5.0,
control_trades: 20,
treatment_trades: 55,
};
// Challenger DD (0.25) > 1.2 * Champion DD (0.10) = 0.12 => should fail
let decision = criteria.should_promote_with_samples(&state, 0.25, 0.10, 5000);
match decision {
PromotionDecision::NotReady { reason } => {
assert!(
reason.contains("drawdown"),
"Expected drawdown reason, got: {reason}"
);
}
other => panic!("Expected NotReady due to drawdown, got {other:?}"),
}
// Challenger DD (0.11) <= 1.2 * 0.10 = 0.12 => should pass
let decision = criteria.should_promote_with_samples(&state, 0.11, 0.10, 5000);
assert_eq!(decision, PromotionDecision::AutoPromoted);
}
#[test]
fn test_promotion_ready_for_approval() {
let criteria = PromotionCriteria {
min_trades: 10,
bayesian_confidence: 0.01,
max_drawdown_ratio: 2.0,
require_manual_approval: true,
};
let state = ThompsonSamplingState {
control_alpha: 10.0,
control_beta: 10.0,
treatment_alpha: 50.0,
treatment_beta: 5.0,
control_trades: 20,
treatment_trades: 55,
};
let decision = criteria.should_promote_with_samples(&state, 0.05, 0.10, 5000);
assert_eq!(decision, PromotionDecision::ReadyForApproval);
}
#[test]
fn test_promotion_auto_promoted() {
let criteria = PromotionCriteria {
min_trades: 10,
bayesian_confidence: 0.01,
max_drawdown_ratio: 2.0,
require_manual_approval: false,
};
let state = ThompsonSamplingState {
control_alpha: 10.0,
control_beta: 10.0,
treatment_alpha: 50.0,
treatment_beta: 5.0,
control_trades: 20,
treatment_trades: 55,
};
let decision = criteria.should_promote_with_samples(&state, 0.05, 0.10, 5000);
assert_eq!(decision, PromotionDecision::AutoPromoted);
}
#[test]
fn test_traffic_splitter_thompson() {
let splitter = TrafficSplitter::new(0.5, 0.5, TrafficSplittingStrategy::ThompsonSampling);
// Thompson state should be initialized
assert!(splitter.thompson_state().is_some());
let mut control_count = 0;
let mut treatment_count = 0;
for _ in 0..500 {
let features = Features::new(vec![1.0, 2.0], vec!["f1".to_owned(), "f2".to_owned()]);
match splitter.assign_group(&features) {
TestGroup::Control => control_count += 1,
TestGroup::Treatment => treatment_count += 1,
}
}
// With default priors Beta(10,1) vs Beta(1,1), control should dominate
// but both groups should get some traffic (exploration)
let total = control_count + treatment_count;
assert_eq!(total, 500);
// Control should get the majority with Beta(10,1)
assert!(
control_count > treatment_count,
"Control ({control_count}) should exceed Treatment ({treatment_count}) with strong prior"
);
// Treatment should still get some traffic (Thompson Sampling explores)
assert!(
treatment_count > 0,
"Treatment should get some exploration traffic"
);
}
#[test]
fn test_traffic_splitter_non_thompson_has_no_state() {
let splitter = TrafficSplitter::new(0.5, 0.5, TrafficSplittingStrategy::Random);
assert!(splitter.thompson_state().is_none());
let splitter = TrafficSplitter::new(0.5, 0.5, TrafficSplittingStrategy::HashBased);
assert!(splitter.thompson_state().is_none());
}
}

View File

@@ -12,34 +12,32 @@
//! - **Performance Monitoring**: Real-time metrics with automatic rollback triggers
//! - **Production Safety**: Circuit breakers, fallback mechanisms, and disaster recovery
#![warn(missing_docs)]
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use std::collections::HashMap;
use std::sync::atomic::{AtomicPtr, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use std::time::{Duration, SystemTime};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use tokio::sync::{RwLock, Mutex};
use uuid::Uuid;
use crate::{MLError, MLResult, ModelType, Features, ModelPrediction, MLModel};
use crate::ModelType;
pub mod registry;
pub mod versioning;
pub mod hot_swap;
pub mod ab_testing;
// Submodules with pre-existing compilation issues — gated until fixed
#[cfg(any())]
pub mod registry;
#[cfg(any())]
pub mod hot_swap;
#[cfg(any())]
pub mod validation;
#[cfg(any())]
pub mod monitoring;
#[cfg(any())]
pub mod endpoints;
// Re-export commonly used types for convenience
pub use versioning::ModelVersion;
pub use ab_testing::{ABTestConfig, ABTestResult};
pub use validation::{ValidationConfig, ValidationResult};
pub use monitoring::MonitoringConfig;
pub use ab_testing::ABTestConfig;
/// Model deployment status
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -163,10 +161,6 @@ pub struct DeploymentConfig {
pub enable_ab_testing: bool,
/// A/B test configuration
pub ab_test_config: Option<ABTestConfig>,
/// Validation pipeline configuration
pub validation_config: ValidationConfig,
/// Monitoring configuration
pub monitoring_config: MonitoringConfig,
/// Rollback configuration
pub rollback_config: RollbackConfig,
/// Canary deployment percentage (0.0 to 1.0)
@@ -182,8 +176,6 @@ impl Default for DeploymentConfig {
Self {
enable_ab_testing: false,
ab_test_config: None,
validation_config: ValidationConfig::default(),
monitoring_config: MonitoringConfig::default(),
rollback_config: RollbackConfig::default(),
canary_percentage: 0.05, // 5% canary traffic
deployment_timeout: Duration::from_secs(300), // 5 minutes
@@ -276,8 +268,6 @@ pub struct DeploymentResult {
pub error_message: Option<String>,
/// Deployment metadata
pub metadata: Option<DeploymentMetadata>,
/// Validation results
pub validation_results: Vec<ValidationResult>,
/// Duration of deployment
pub deployment_duration: Duration,
/// Events generated during deployment
@@ -296,7 +286,6 @@ impl DeploymentResult {
deployment_id: Some(deployment_id),
error_message: None,
metadata: Some(metadata),
validation_results: Vec::new(),
deployment_duration: duration,
events: Vec::new(),
}
@@ -309,17 +298,11 @@ impl DeploymentResult {
deployment_id: None,
error_message: Some(error),
metadata: None,
validation_results: Vec::new(),
deployment_duration: duration,
events: Vec::new(),
}
}
/// Add validation result
pub fn add_validation_result(&mut self, result: ValidationResult) {
self.validation_results.push(result);
}
/// Add event
pub fn add_event(&mut self, event: DeploymentEvent) {
self.events.push(event);

View File

@@ -117,11 +117,10 @@ impl ModelVersion {
}
// For the same major version, newer minor/patch versions are backward compatible
match (self.minor.cmp(&other.minor), self.patch.cmp(&other.patch)) {
(Ordering::Greater, _) => true,
(Ordering::Equal, Ordering::Greater | Ordering::Equal) => true,
_ => false,
}
matches!(
(self.minor.cmp(&other.minor), self.patch.cmp(&other.patch)),
(Ordering::Greater, _) | (Ordering::Equal, Ordering::Greater | Ordering::Equal)
)
}
/// Check if this version represents a breaking change from another version
@@ -202,19 +201,22 @@ impl Ord for ModelVersion {
// Compare major version first
match self.major.cmp(&other.major) {
Ordering::Equal => {}
other => return other,
Ordering::Less => return Ordering::Less,
Ordering::Greater => return Ordering::Greater,
}
// Compare minor version
match self.minor.cmp(&other.minor) {
Ordering::Equal => {}
other => return other,
Ordering::Less => return Ordering::Less,
Ordering::Greater => return Ordering::Greater,
}
// Compare patch version
match self.patch.cmp(&other.patch) {
Ordering::Equal => {}
other => return other,
Ordering::Less => return Ordering::Less,
Ordering::Greater => return Ordering::Greater,
}
// Compare pre-release versions
@@ -272,7 +274,7 @@ pub struct VersionEntry {
/// Version information
pub version: ModelVersion,
/// Deployment timestamp
pub deployed_at: std::time::SystemTime,
pub deployed_at: SystemTime,
/// Change description
pub change_description: String,
/// Deployment author
@@ -383,10 +385,10 @@ pub mod version_utils {
use super::*;
/// Find the highest compatible version from a list
pub fn find_highest_compatible(
versions: &[ModelVersion],
pub fn find_highest_compatible<'a>(
versions: &'a [ModelVersion],
constraint: &VersionConstraint,
) -> Option<&ModelVersion> {
) -> Option<&'a ModelVersion> {
versions
.iter()
.filter(|version| constraint.satisfies(version))
@@ -500,7 +502,7 @@ mod tests {
let entry1 = VersionEntry {
version: ModelVersion::new(1, 0, 0),
deployed_at: std::time::SystemTime::now(),
deployed_at: SystemTime::now(),
change_description: "Initial release".to_owned(),
deployed_by: "user1".to_owned(),
performance_metrics: HashMap::new(),
@@ -509,7 +511,7 @@ mod tests {
let entry2 = VersionEntry {
version: ModelVersion::new(1, 1, 0),
deployed_at: std::time::SystemTime::now(),
deployed_at: SystemTime::now(),
change_description: "Feature addition".to_owned(),
deployed_by: "user2".to_owned(),
performance_metrics: HashMap::new(),

View File

@@ -792,6 +792,7 @@ pub mod config; // Configuration module for feature extraction
pub mod cuda_compat; // CUDA-compatible operations (manual sigmoid, etc.)
pub mod cuda_pipeline; // GPU data pre-upload pipeline for DQN/PPO trainers
pub mod data_loaders; // Data loaders for ML training
pub mod deployment; // Model deployment, A/B testing, versioning, hot-swap
pub mod dqn;
pub mod gradient_accumulation; // Gradient accumulation for mini-batch training
pub mod gradient_utils; // Gradient utilities (clipping, monitoring)