Machine Learning Pipeline Orchestration with MLflow and Kubeflow: End-to-End ML Operations and Automation

Modern machine learning operations require sophisticated orchestration platforms that can manage the entire ML lifecycle from experimentation to production deployment. MLflow and Kubeflow represent two complementary approaches to ML pipeline orchestration: MLflow excels at experiment tracking and model management, while Kubeflow provides Kubernetes-native workflow orchestration and scalable ML infrastructure.

This comprehensive guide explores advanced techniques for implementing enterprise-grade ML pipeline orchestration, combining the strengths of both platforms to create robust, scalable, and maintainable ML operations workflows.

Understanding MLflow and Kubeflow Architecture

MLflow Components and Integration

MLflow provides four main components that work together to manage the ML lifecycle: Tracking, Projects, Models, and Model Registry.

# Advanced MLflow tracking and experiment management
import mlflow
import mlflow.sklearn
import mlflow.pytorch
import mlflow.tensorflow
from mlflow.tracking import MlflowClient
from mlflow.entities import ViewType
import pandas as pd
import numpy as np
from typing import Dict, List, Any, Optional, Tuple
import logging
import json
import os
from datetime import datetime
import joblib
import pickle

class MLflowExperimentManager:
    """Advanced MLflow experiment management and tracking"""
    
    def __init__(self, tracking_uri: str, registry_uri: Optional[str] = None):
        self.tracking_uri = tracking_uri
        self.registry_uri = registry_uri or tracking_uri
        
        mlflow.set_tracking_uri(self.tracking_uri)
        mlflow.set_registry_uri(self.registry_uri)
        
        self.client = MlflowClient(tracking_uri=self.tracking_uri)
        
    def create_experiment(self, experiment_name: str, 
                         artifact_location: Optional[str] = None,
                         tags: Optional[Dict[str, str]] = None) -> str:
        """Create a new experiment with metadata"""
        
        try:
            experiment_id = mlflow.create_experiment(
                name=experiment_name,
                artifact_location=artifact_location,
                tags=tags or {}
            )
            
            logging.info(f"Created experiment: {experiment_name} (ID: {experiment_id})")
            return experiment_id
            
        except mlflow.exceptions.MlflowException as e:
            if "already exists" in str(e):
                experiment = mlflow.get_experiment_by_name(experiment_name)
                logging.info(f"Using existing experiment: {experiment_name} (ID: {experiment.experiment_id})")
                return experiment.experiment_id
            else:
                raise
    
    def start_run_with_context(self, experiment_id: str, 
                              run_name: Optional[str] = None,
                              tags: Optional[Dict[str, str]] = None,
                              nested: bool = False) -> mlflow.ActiveRun:
        """Start MLflow run with comprehensive context"""
        
        # Enhanced tags with system information
        system_tags = {
            "mlflow.user": os.environ.get("USER", "unknown"),
            "mlflow.source.type": "LOCAL",
            "pipeline.stage": tags.get("stage", "unknown") if tags else "unknown",
            "pipeline.version": tags.get("version", "1.0") if tags else "1.0",
            "environment": tags.get("environment", "development") if tags else "development"
        }
        
        if tags:
            system_tags.update(tags)
        
        return mlflow.start_run(
            experiment_id=experiment_id,
            run_name=run_name,
            tags=system_tags,
            nested=nested
        )
    
    def log_comprehensive_metrics(self, metrics: Dict[str, float], 
                                 step: Optional[int] = None,
                                 timestamp: Optional[int] = None):
        """Log metrics with enhanced metadata"""
        
        for metric_name, value in metrics.items():
            # Validate metric value
            if not isinstance(value, (int, float)) or np.isnan(value) or np.isinf(value):
                logging.warning(f"Invalid metric value for {metric_name}: {value}")
                continue
                
            mlflow.log_metric(metric_name, value, step=step, timestamp=timestamp)
    
    def log_model_with_signature(self, model: Any, 
                                artifact_path: str,
                                framework: str,
                                input_example: Optional[Any] = None,
                                signature: Optional[mlflow.models.ModelSignature] = None,
                                conda_env: Optional[str] = None,
                                requirements_file: Optional[str] = None,
                                extra_files: Optional[List[str]] = None):
        """Log model with comprehensive metadata and signature"""
        
        # Generate model signature if not provided
        if signature is None and input_example is not None:
            if framework == "sklearn":
                predictions = model.predict(input_example)
                signature = mlflow.models.infer_signature(input_example, predictions)
            elif framework == "pytorch":
                import torch
                if isinstance(input_example, torch.Tensor):
                    with torch.no_grad():
                        predictions = model(input_example)
                    signature = mlflow.models.infer_signature(
                        input_example.numpy(), predictions.numpy()
                    )
        
        # Log model based on framework
        if framework == "sklearn":
            mlflow.sklearn.log_model(
                sk_model=model,
                artifact_path=artifact_path,
                conda_env=conda_env,
                signature=signature,
                input_example=input_example,
                extra_files=extra_files
            )
        elif framework == "pytorch":
            mlflow.pytorch.log_model(
                pytorch_model=model,
                artifact_path=artifact_path,
                conda_env=conda_env,
                signature=signature,
                input_example=input_example,
                extra_files=extra_files
            )
        elif framework == "tensorflow":
            mlflow.tensorflow.log_model(
                tf_saved_model_dir=model,
                tf_meta_graph_tags=None,
                tf_signature_def_key=None,
                artifact_path=artifact_path,
                conda_env=conda_env,
                signature=signature,
                input_example=input_example
            )
        else:
            # Generic model logging
            mlflow.log_artifact(model, artifact_path)
    
    def register_model(self, model_uri: str, 
                      model_name: str,
                      description: Optional[str] = None,
                      tags: Optional[Dict[str, str]] = None) -> mlflow.entities.ModelVersion:
        """Register model in MLflow Model Registry"""
        
        model_version = mlflow.register_model(
            model_uri=model_uri,
            name=model_name
        )
        
        # Update model version with description and tags
        if description:
            self.client.update_model_version(
                name=model_name,
                version=model_version.version,
                description=description
            )
        
        if tags:
            for key, value in tags.items():
                self.client.set_model_version_tag(
                    name=model_name,
                    version=model_version.version,
                    key=key,
                    value=value
                )
        
        logging.info(f"Registered model {model_name} version {model_version.version}")
        return model_version
    
    def promote_model(self, model_name: str, 
                     version: str,
                     stage: str,
                     archive_existing: bool = True) -> None:
        """Promote model to specified stage"""
        
        if archive_existing and stage.lower() == "production":
            # Archive existing production models
            production_models = self.client.get_latest_versions(
                name=model_name,
                stages=["Production"]
            )
            
            for model in production_models:
                self.client.transition_model_version_stage(
                    name=model_name,
                    version=model.version,
                    stage="Archived"
                )
                logging.info(f"Archived model {model_name} version {model.version}")
        
        # Promote new model
        self.client.transition_model_version_stage(
            name=model_name,
            version=version,
            stage=stage
        )
        
        logging.info(f"Promoted model {model_name} version {version} to {stage}")
    
    def compare_experiments(self, experiment_ids: List[str], 
                          metric_names: List[str]) -> pd.DataFrame:
        """Compare metrics across multiple experiments"""
        
        all_runs = []
        
        for experiment_id in experiment_ids:
            runs = self.client.search_runs(
                experiment_ids=[experiment_id],
                filter_string="",
                run_view_type=ViewType.ACTIVE_ONLY
            )
            
            for run in runs:
                run_data = {
                    'experiment_id': experiment_id,
                    'run_id': run.info.run_id,
                    'run_name': run.data.tags.get('mlflow.runName', 'Unnamed'),
                    'status': run.info.status,
                    'start_time': run.info.start_time,
                    'end_time': run.info.end_time
                }
                
                # Add requested metrics
                for metric_name in metric_names:
                    metric_value = run.data.metrics.get(metric_name)
                    run_data[metric_name] = metric_value
                
                # Add important parameters
                for param_name, param_value in run.data.params.items():
                    run_data[f"param_{param_name}"] = param_value
                
                all_runs.append(run_data)
        
        return pd.DataFrame(all_runs)
    
    def get_best_run(self, experiment_id: str, 
                    metric_name: str,
                    mode: str = "max") -> Optional[mlflow.entities.Run]:
        """Get best run based on specified metric"""
        
        runs = self.client.search_runs(
            experiment_ids=[experiment_id],
            filter_string="",
            run_view_type=ViewType.ACTIVE_ONLY,
            order_by=[f"metrics.{metric_name} {'DESC' if mode == 'max' else 'ASC'}"],
            max_results=1
        )
        
        return runs[0] if runs else None

class MLflowModelManager:
    """Advanced MLflow model management and deployment"""
    
    def __init__(self, mlflow_manager: MLflowExperimentManager):
        self.mlflow_manager = mlflow_manager
        self.client = mlflow_manager.client
    
    def create_model_serving_config(self, model_name: str, 
                                   model_version: str,
                                   serving_platform: str = "kubernetes") -> Dict[str, Any]:
        """Create model serving configuration"""
        
        model_uri = f"models:/{model_name}/{model_version}"
        
        if serving_platform == "kubernetes":
            config = {
                "apiVersion": "serving.kubeflow.org/v1beta1",
                "kind": "InferenceService",
                "metadata": {
                    "name": f"{model_name.lower()}-v{model_version}",
                    "annotations": {
                        "serving.kubeflow.org/autoscalerClass": "hpa",
                        "serving.kubeflow.org/metric": "cpu",
                        "serving.kubeflow.org/target": "80"
                    }
                },
                "spec": {
                    "predictor": {
                        "sklearn": {
                            "storageUri": model_uri,
                            "resources": {
                                "requests": {
                                    "cpu": "100m",
                                    "memory": "128Mi"
                                },
                                "limits": {
                                    "cpu": "1",
                                    "memory": "1Gi"
                                }
                            }
                        }
                    }
                }
            }
        elif serving_platform == "sagemaker":
            config = {
                "ModelName": f"{model_name}-v{model_version}",
                "PrimaryContainer": {
                    "Image": "your-mlflow-container-uri",
                    "ModelDataUrl": model_uri,
                    "Environment": {
                        "MLFLOW_TRACKING_URI": self.mlflow_manager.tracking_uri,
                        "MLFLOW_MODEL_URI": model_uri
                    }
                }
            }
        else:
            raise ValueError(f"Unsupported serving platform: {serving_platform}")
        
        return config
    
    def validate_model_quality(self, model_name: str, 
                              model_version: str,
                              validation_data: pd.DataFrame,
                              quality_thresholds: Dict[str, float]) -> Dict[str, Any]:
        """Validate model quality before deployment"""
        
        # Load model
        model_uri = f"models:/{model_name}/{model_version}"
        model = mlflow.pyfunc.load_model(model_uri)
        
        # Generate predictions
        X = validation_data.drop(['target'], axis=1) if 'target' in validation_data.columns else validation_data
        y_true = validation_data['target'] if 'target' in validation_data.columns else None
        y_pred = model.predict(X)
        
        validation_results = {
            "model_name": model_name,
            "model_version": model_version,
            "validation_timestamp": datetime.utcnow().isoformat(),
            "sample_size": len(validation_data),
            "quality_checks": {}
        }
        
        # Calculate quality metrics based on problem type
        if y_true is not None:
            # Determine if classification or regression
            if len(np.unique(y_true)) <= 10:  # Assume classification
                from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
                
                metrics = {
                    "accuracy": accuracy_score(y_true, y_pred),
                    "precision": precision_score(y_true, y_pred, average='weighted'),
                    "recall": recall_score(y_true, y_pred, average='weighted'),
                    "f1_score": f1_score(y_true, y_pred, average='weighted')
                }
            else:  # Assume regression
                from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
                
                metrics = {
                    "mse": mean_squared_error(y_true, y_pred),
                    "mae": mean_absolute_error(y_true, y_pred),
                    "r2_score": r2_score(y_true, y_pred)
                }
            
            # Check against thresholds
            for metric_name, metric_value in metrics.items():
                threshold = quality_thresholds.get(metric_name)
                if threshold is not None:
                    if metric_name in ["mse", "mae"]:  # Lower is better
                        passed = metric_value <= threshold
                    else:  # Higher is better
                        passed = metric_value >= threshold
                    
                    validation_results["quality_checks"][metric_name] = {
                        "value": metric_value,
                        "threshold": threshold,
                        "passed": passed
                    }
        
        # Check for data drift
        validation_results["data_drift"] = self._check_data_drift(X, model_name)
        
        # Overall validation status
        all_checks_passed = all(
            check["passed"] for check in validation_results["quality_checks"].values()
        )
        validation_results["overall_status"] = "PASSED" if all_checks_passed else "FAILED"
        
        return validation_results
    
    def _check_data_drift(self, current_data: pd.DataFrame, model_name: str) -> Dict[str, Any]:
        """Check for data drift compared to training data"""
        
        # This is a simplified drift detection
        # In practice, you would use more sophisticated methods like KS test, PSI, etc.
        
        drift_results = {
            "drift_detected": False,
            "drift_score": 0.0,
            "features_with_drift": []
        }
        
        try:
            # Get training data statistics (would be stored during training)
            # For now, we'll simulate this
            for column in current_data.select_dtypes(include=[np.number]).columns:
                current_mean = current_data[column].mean()
                current_std = current_data[column].std()
                
                # Simulate training statistics (in practice, load from MLflow)
                training_mean = current_mean * (1 + np.random.normal(0, 0.1))
                training_std = current_std * (1 + np.random.normal(0, 0.1))
                
                # Simple drift detection based on mean and std changes
                mean_change = abs(current_mean - training_mean) / training_mean
                std_change = abs(current_std - training_std) / training_std
                
                if mean_change > 0.2 or std_change > 0.2:  # 20% threshold
                    drift_results["features_with_drift"].append({
                        "feature": column,
                        "mean_change": mean_change,
                        "std_change": std_change
                    })
            
            if drift_results["features_with_drift"]:
                drift_results["drift_detected"] = True
                drift_results["drift_score"] = len(drift_results["features_with_drift"]) / len(current_data.columns)
        
        except Exception as e:
            logging.warning(f"Drift detection failed: {e}")
        
        return drift_results
    
    def create_model_monitoring_config(self, model_name: str, 
                                     model_version: str) -> Dict[str, Any]:
        """Create monitoring configuration for deployed model"""
        
        monitoring_config = {
            "model_name": model_name,
            "model_version": model_version,
            "monitoring_schedule": "0 */4 * * *",  # Every 4 hours
            "metrics_to_track": [
                "prediction_latency",
                "throughput",
                "error_rate",
                "data_drift",
                "model_accuracy"
            ],
            "alerts": {
                "latency_threshold_ms": 1000,
                "error_rate_threshold": 0.05,
                "drift_threshold": 0.3,
                "accuracy_degradation_threshold": 0.1
            },
            "data_collection": {
                "sample_rate": 0.1,  # Sample 10% of predictions
                "store_predictions": True,
                "store_features": True,
                "retention_days": 90
            }
        }
        
        return monitoring_config

Kubeflow Pipeline Architecture

Kubeflow provides Kubernetes-native ML workflow orchestration with components for pipeline definition, execution, and monitoring.

# Advanced Kubeflow pipeline components and orchestration
import kfp
from kfp import dsl, compiler
from kfp.components import func_to_container_op, InputPath, OutputPath
from kubernetes import client, config
import yaml
import os
from typing import Dict, List, Any, Optional
import logging

class KubeflowPipelineManager:
    """Advanced Kubeflow pipeline management and orchestration"""
    
    def __init__(self, kubeflow_endpoint: str, namespace: str = "kubeflow"):
        self.endpoint = kubeflow_endpoint
        self.namespace = namespace
        self.client = kfp.Client(host=kubeflow_endpoint)
        
    def create_data_processing_component(self) -> dsl.ContainerOp:
        """Create data processing component for ML pipeline"""
        
        @func_to_container_op
        def process_data(
            input_data_path: InputPath(str),
            output_data_path: OutputPath(str),
            validation_split: float = 0.2,
            test_split: float = 0.1,
            random_seed: int = 42
        ):
            import pandas as pd
            import numpy as np
            from sklearn.model_selection import train_test_split
            from sklearn.preprocessing import StandardScaler, LabelEncoder
            import pickle
            import os
            
            # Load data
            df = pd.read_csv(input_data_path)
            
            # Data preprocessing
            # Handle missing values
            df = df.fillna(df.mean() if df.select_dtypes(include=[np.number]).shape[1] > 0 else df.mode().iloc[0])
            
            # Encode categorical variables
            categorical_columns = df.select_dtypes(include=['object']).columns
            label_encoders = {}
            
            for col in categorical_columns:
                if col != 'target':  # Assume 'target' is the target column
                    le = LabelEncoder()
                    df[col] = le.fit_transform(df[col].astype(str))
                    label_encoders[col] = le
            
            # Separate features and target
            if 'target' in df.columns:
                X = df.drop('target', axis=1)
                y = df['target']
                
                # Split data
                X_temp, X_test, y_temp, y_test = train_test_split(
                    X, y, test_size=test_split, random_state=random_seed, stratify=y
                )
                
                X_train, X_val, y_train, y_val = train_test_split(
                    X_temp, y_temp, test_size=validation_split/(1-test_split), 
                    random_state=random_seed, stratify=y_temp
                )
                
                # Scale features
                scaler = StandardScaler()
                X_train_scaled = scaler.fit_transform(X_train)
                X_val_scaled = scaler.transform(X_val)
                X_test_scaled = scaler.transform(X_test)
                
                # Save processed data
                os.makedirs(output_data_path, exist_ok=True)
                
                np.save(os.path.join(output_data_path, 'X_train.npy'), X_train_scaled)
                np.save(os.path.join(output_data_path, 'X_val.npy'), X_val_scaled)
                np.save(os.path.join(output_data_path, 'X_test.npy'), X_test_scaled)
                np.save(os.path.join(output_data_path, 'y_train.npy'), y_train.values)
                np.save(os.path.join(output_data_path, 'y_val.npy'), y_val.values)
                np.save(os.path.join(output_data_path, 'y_test.npy'), y_test.values)
                
                # Save preprocessing artifacts
                with open(os.path.join(output_data_path, 'scaler.pkl'), 'wb') as f:
                    pickle.dump(scaler, f)
                
                with open(os.path.join(output_data_path, 'label_encoders.pkl'), 'wb') as f:
                    pickle.dump(label_encoders, f)
                
                # Save feature names
                feature_names = X.columns.tolist()
                with open(os.path.join(output_data_path, 'feature_names.txt'), 'w') as f:
                    f.write('\n'.join(feature_names))
                
                print(f"Data processing complete. Train: {X_train.shape}, Val: {X_val.shape}, Test: {X_test.shape}")
            
            else:
                raise ValueError("Target column 'target' not found in dataset")
        
        return process_data
    
    def create_model_training_component(self) -> dsl.ContainerOp:
        """Create model training component"""
        
        @func_to_container_op
        def train_model(
            data_path: InputPath(str),
            model_output_path: OutputPath(str),
            algorithm: str = "random_forest",
            hyperparameters: str = "{}",
            mlflow_tracking_uri: str = "",
            experiment_name: str = "kubeflow_experiment"
        ):
            import numpy as np
            import json
            import pickle
            import os
            import mlflow
            import mlflow.sklearn
            from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
            from sklearn.linear_model import LogisticRegression
            from sklearn.svm import SVC
            from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
            from sklearn.metrics import classification_report, confusion_matrix
            
            # Set up MLflow tracking
            if mlflow_tracking_uri:
                mlflow.set_tracking_uri(mlflow_tracking_uri)
                mlflow.set_experiment(experiment_name)
            
            # Load data
            X_train = np.load(os.path.join(data_path, 'X_train.npy'))
            X_val = np.load(os.path.join(data_path, 'X_val.npy'))
            y_train = np.load(os.path.join(data_path, 'y_train.npy'))
            y_val = np.load(os.path.join(data_path, 'y_val.npy'))
            
            # Parse hyperparameters
            params = json.loads(hyperparameters) if hyperparameters else {}
            
            # Initialize model based on algorithm
            if algorithm == "random_forest":
                model = RandomForestClassifier(
                    n_estimators=params.get('n_estimators', 100),
                    max_depth=params.get('max_depth', None),
                    min_samples_split=params.get('min_samples_split', 2),
                    random_state=42
                )
            elif algorithm == "gradient_boosting":
                model = GradientBoostingClassifier(
                    n_estimators=params.get('n_estimators', 100),
                    learning_rate=params.get('learning_rate', 0.1),
                    max_depth=params.get('max_depth', 3),
                    random_state=42
                )
            elif algorithm == "logistic_regression":
                model = LogisticRegression(
                    C=params.get('C', 1.0),
                    max_iter=params.get('max_iter', 1000),
                    random_state=42
                )
            elif algorithm == "svm":
                model = SVC(
                    C=params.get('C', 1.0),
                    kernel=params.get('kernel', 'rbf'),
                    probability=True,
                    random_state=42
                )
            else:
                raise ValueError(f"Unsupported algorithm: {algorithm}")
            
            # Start MLflow run
            with mlflow.start_run(run_name=f"kubeflow_{algorithm}_training"):
                # Log parameters
                mlflow.log_param("algorithm", algorithm)
                for param_name, param_value in params.items():
                    mlflow.log_param(param_name, param_value)
                
                # Train model
                model.fit(X_train, y_train)
                
                # Evaluate model
                train_pred = model.predict(X_train)
                val_pred = model.predict(X_val)
                
                # Calculate metrics
                train_accuracy = accuracy_score(y_train, train_pred)
                val_accuracy = accuracy_score(y_val, val_pred)
                val_precision = precision_score(y_val, val_pred, average='weighted')
                val_recall = recall_score(y_val, val_pred, average='weighted')
                val_f1 = f1_score(y_val, val_pred, average='weighted')
                
                # Log metrics
                mlflow.log_metric("train_accuracy", train_accuracy)
                mlflow.log_metric("val_accuracy", val_accuracy)
                mlflow.log_metric("val_precision", val_precision)
                mlflow.log_metric("val_recall", val_recall)
                mlflow.log_metric("val_f1_score", val_f1)
                
                # Log classification report
                class_report = classification_report(y_val, val_pred, output_dict=True)
                mlflow.log_dict(class_report, "classification_report.json")
                
                # Save model
                os.makedirs(model_output_path, exist_ok=True)
                model_path = os.path.join(model_output_path, 'model.pkl')
                
                with open(model_path, 'wb') as f:
                    pickle.dump(model, f)
                
                # Log model to MLflow
                mlflow.sklearn.log_model(
                    sk_model=model,
                    artifact_path="model",
                    registered_model_name=f"{experiment_name}_{algorithm}"
                )
                
                # Save metrics for pipeline
                metrics = {
                    "train_accuracy": train_accuracy,
                    "val_accuracy": val_accuracy,
                    "val_precision": val_precision,
                    "val_recall": val_recall,
                    "val_f1_score": val_f1
                }
                
                with open(os.path.join(model_output_path, 'metrics.json'), 'w') as f:
                    json.dump(metrics, f)
                
                print(f"Model training complete. Validation accuracy: {val_accuracy:.4f}")
        
        return train_model
    
    def create_model_evaluation_component(self) -> dsl.ContainerOp:
        """Create model evaluation component"""
        
        @func_to_container_op
        def evaluate_model(
            data_path: InputPath(str),
            model_path: InputPath(str),
            evaluation_output_path: OutputPath(str),
            quality_threshold: float = 0.8
        ):
            import numpy as np
            import json
            import pickle
            import os
            from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
            from sklearn.metrics import roc_auc_score, classification_report, confusion_matrix
            import matplotlib.pyplot as plt
            import seaborn as sns
            
            # Load test data
            X_test = np.load(os.path.join(data_path, 'X_test.npy'))
            y_test = np.load(os.path.join(data_path, 'y_test.npy'))
            
            # Load model
            with open(os.path.join(model_path, 'model.pkl'), 'rb') as f:
                model = pickle.load(f)
            
            # Make predictions
            y_pred = model.predict(X_test)
            y_pred_proba = model.predict_proba(X_test) if hasattr(model, 'predict_proba') else None
            
            # Calculate comprehensive metrics
            test_accuracy = accuracy_score(y_test, y_pred)
            test_precision = precision_score(y_test, y_pred, average='weighted')
            test_recall = recall_score(y_test, y_pred, average='weighted')
            test_f1 = f1_score(y_test, y_pred, average='weighted')
            
            evaluation_results = {
                "test_accuracy": test_accuracy,
                "test_precision": test_precision,
                "test_recall": test_recall,
                "test_f1_score": test_f1,
                "quality_threshold": quality_threshold,
                "quality_check_passed": test_accuracy >= quality_threshold
            }
            
            # Calculate AUC if binary classification
            unique_classes = np.unique(y_test)
            if len(unique_classes) == 2 and y_pred_proba is not None:
                test_auc = roc_auc_score(y_test, y_pred_proba[:, 1])
                evaluation_results["test_auc"] = test_auc
            
            # Generate detailed classification report
            class_report = classification_report(y_test, y_pred, output_dict=True)
            
            # Create output directory
            os.makedirs(evaluation_output_path, exist_ok=True)
            
            # Save evaluation results
            with open(os.path.join(evaluation_output_path, 'evaluation_results.json'), 'w') as f:
                json.dump(evaluation_results, f, indent=2)
            
            with open(os.path.join(evaluation_output_path, 'classification_report.json'), 'w') as f:
                json.dump(class_report, f, indent=2)
            
            # Generate confusion matrix plot
            cm = confusion_matrix(y_test, y_pred)
            plt.figure(figsize=(8, 6))
            sns.heatmap(cm, annot=True, fmt='d', cmap='Blues')
            plt.title('Confusion Matrix')
            plt.ylabel('True Label')
            plt.xlabel('Predicted Label')
            plt.savefig(os.path.join(evaluation_output_path, 'confusion_matrix.png'))
            plt.close()
            
            print(f"Model evaluation complete. Test accuracy: {test_accuracy:.4f}")
            print(f"Quality check: {'PASSED' if evaluation_results['quality_check_passed'] else 'FAILED'}")
            
            # Exit with non-zero code if quality check fails
            if not evaluation_results['quality_check_passed']:
                raise ValueError(f"Model quality check failed. Accuracy {test_accuracy:.4f} < threshold {quality_threshold}")
        
        return evaluate_model
    
    def create_model_deployment_component(self) -> dsl.ContainerOp:
        """Create model deployment component"""
        
        @func_to_container_op
        def deploy_model(
            model_path: InputPath(str),
            evaluation_path: InputPath(str),
            deployment_output_path: OutputPath(str),
            model_name: str = "ml_model",
            deployment_target: str = "kubernetes",
            mlflow_tracking_uri: str = ""
        ):
            import json
            import os
            import mlflow
            from mlflow.tracking import MlflowClient
            import yaml
            
            # Load evaluation results
            with open(os.path.join(evaluation_path, 'evaluation_results.json'), 'r') as f:
                evaluation_results = json.loads(f.read())
            
            # Check if quality check passed
            if not evaluation_results.get('quality_check_passed', False):
                raise ValueError("Cannot deploy model - quality check failed")
            
            # Set up MLflow client
            if mlflow_tracking_uri:
                mlflow.set_tracking_uri(mlflow_tracking_uri)
                client = MlflowClient()
            
            # Create deployment configuration
            deployment_config = {
                "model_name": model_name,
                "deployment_target": deployment_target,
                "deployment_timestamp": "2025-01-01T00:00:00Z",  # Would use actual timestamp
                "model_metrics": evaluation_results,
                "deployment_status": "success"
            }
            
            if deployment_target == "kubernetes":
                # Create Kubernetes deployment configuration
                k8s_config = {
                    "apiVersion": "apps/v1",
                    "kind": "Deployment",
                    "metadata": {
                        "name": f"{model_name}-deployment",
                        "labels": {
                            "app": model_name,
                            "version": "latest"
                        }
                    },
                    "spec": {
                        "replicas": 3,
                        "selector": {
                            "matchLabels": {
                                "app": model_name
                            }
                        },
                        "template": {
                            "metadata": {
                                "labels": {
                                    "app": model_name
                                }
                            },
                            "spec": {
                                "containers": [{
                                    "name": model_name,
                                    "image": f"your-registry/{model_name}:latest",
                                    "ports": [{
                                        "containerPort": 8080
                                    }],
                                    "env": [{
                                        "name": "MODEL_PATH",
                                        "value": "/app/model"
                                    }],
                                    "resources": {
                                        "requests": {
                                            "cpu": "100m",
                                            "memory": "256Mi"
                                        },
                                        "limits": {
                                            "cpu": "1",
                                            "memory": "1Gi"
                                        }
                                    }
                                }]
                            }
                        }
                    }
                }
                
                deployment_config["kubernetes_config"] = k8s_config
            
            # Create output directory
            os.makedirs(deployment_output_path, exist_ok=True)
            
            # Save deployment configuration
            with open(os.path.join(deployment_output_path, 'deployment_config.json'), 'w') as f:
                json.dump(deployment_config, f, indent=2)
            
            if deployment_target == "kubernetes":
                with open(os.path.join(deployment_output_path, 'kubernetes_deployment.yaml'), 'w') as f:
                    yaml.dump(k8s_config, f, default_flow_style=False)
            
            print(f"Model deployment configuration created for {deployment_target}")
        
        return deploy_model
    
    @dsl.pipeline(
        name='End-to-End ML Pipeline',
        description='Complete ML pipeline with data processing, training, evaluation, and deployment'
    )
    def create_end_to_end_pipeline(
        input_data_path: str,
        model_name: str = "ml_model",
        algorithm: str = "random_forest",
        hyperparameters: str = "{}",
        quality_threshold: float = 0.8,
        deployment_target: str = "kubernetes",
        mlflow_tracking_uri: str = ""
    ):
        """Create comprehensive ML pipeline"""
        
        # Data processing step
        data_processing_op = self.create_data_processing_component()(
            input_data_path=input_data_path
        )
        
        # Model training step
        training_op = self.create_model_training_component()(
            data_path=data_processing_op.output,
            algorithm=algorithm,
            hyperparameters=hyperparameters,
            mlflow_tracking_uri=mlflow_tracking_uri,
            experiment_name=model_name
        )
        
        # Model evaluation step
        evaluation_op = self.create_model_evaluation_component()(
            data_path=data_processing_op.output,
            model_path=training_op.output,
            quality_threshold=quality_threshold
        )
        
        # Model deployment step (conditional on evaluation success)
        deployment_op = self.create_model_deployment_component()(
            model_path=training_op.output,
            evaluation_path=evaluation_op.output,
            model_name=model_name,
            deployment_target=deployment_target,
            mlflow_tracking_uri=mlflow_tracking_uri
        )
        
        # Set dependencies
        training_op.after(data_processing_op)
        evaluation_op.after(training_op)
        deployment_op.after(evaluation_op)
        
        # Configure resource requirements
        data_processing_op.container.set_memory_request('1Gi').set_cpu_request('0.5')
        training_op.container.set_memory_request('2Gi').set_cpu_request('1')
        evaluation_op.container.set_memory_request('1Gi').set_cpu_request('0.5')
        deployment_op.container.set_memory_request('512Mi').set_cpu_request('0.25')
        
        return deployment_op
    
    def submit_pipeline(self, pipeline_func: callable, 
                       experiment_name: str,
                       run_name: Optional[str] = None,
                       parameters: Optional[Dict[str, Any]] = None) -> str:
        """Submit pipeline for execution"""
        
        # Compile pipeline
        compiler.Compiler().compile(pipeline_func, 'pipeline.yaml')
        
        # Create experiment if it doesn't exist
        try:
            experiment = self.client.create_experiment(name=experiment_name)
        except:
            experiment = self.client.get_experiment(experiment_name=experiment_name)
        
        # Submit run
        run = self.client.run_pipeline(
            experiment_id=experiment.id,
            job_name=run_name or f"pipeline_run_{int(datetime.now().timestamp())}",
            pipeline_package_path='pipeline.yaml',
            params=parameters or {}
        )
        
        logging.info(f"Pipeline submitted with run ID: {run.id}")
        return run.id
    
    def monitor_pipeline_run(self, run_id: str) -> Dict[str, Any]:
        """Monitor pipeline run status and progress"""
        
        run_detail = self.client.get_run(run_id)
        
        status = {
            "run_id": run_id,
            "status": run_detail.run.status,
            "created_at": run_detail.run.created_at,
            "finished_at": run_detail.run.finished_at,
            "pipeline_spec": run_detail.pipeline_spec.pipeline_name if run_detail.pipeline_spec else None,
            "workflow_manifest": run_detail.pipeline_runtime.workflow_manifest is not None
        }
        
        return status
    
    def get_pipeline_metrics(self, run_id: str) -> Dict[str, Any]:
        """Get metrics from pipeline run"""
        
        # This would extract metrics from the pipeline artifacts
        # Implementation depends on how metrics are stored in your pipeline
        
        metrics = {
            "run_id": run_id,
            "metrics": {},
            "artifacts": []
        }
        
        try:
            # Get run details and extract metrics from artifacts
            run_detail = self.client.get_run(run_id)
            
            # Extract metrics from workflow (this is a simplified example)
            if run_detail.pipeline_runtime and run_detail.pipeline_runtime.workflow_manifest:
                # Parse workflow manifest to extract metrics
                # This would be more complex in a real implementation
                pass
            
        except Exception as e:
            logging.warning(f"Could not retrieve pipeline metrics: {e}")
        
        return metrics

Advanced Model Management and Deployment

MLflow Model Registry Integration

# Advanced model registry operations and deployment automation
class ModelRegistryManager:
    """Advanced MLflow Model Registry management"""
    
    def __init__(self, mlflow_manager: MLflowExperimentManager):
        self.mlflow_manager = mlflow_manager
        self.client = mlflow_manager.client
        
    def create_model_governance_workflow(self, model_name: str) -> Dict[str, Any]:
        """Create model governance workflow with approval gates"""
        
        workflow = {
            "model_name": model_name,
            "stages": {
                "None": {
                    "description": "Newly registered model versions",
                    "auto_transitions": [],
                    "approval_required": False
                },
                "Staging": {
                    "description": "Model versions under testing",
                    "auto_transitions": ["automated_tests_passed"],
                    "approval_required": True,
                    "approvers": ["data_science_lead", "ml_engineer"],
                    "requirements": [
                        "performance_validation",
                        "security_scan",
                        "data_drift_check"
                    ]
                },
                "Production": {
                    "description": "Production-ready model versions",
                    "auto_transitions": [],
                    "approval_required": True,
                    "approvers": ["data_science_lead", "engineering_manager", "product_owner"],
                    "requirements": [
                        "staging_validation",
                        "load_testing",
                        "business_approval",
                        "monitoring_setup"
                    ]
                },
                "Archived": {
                    "description": "Deprecated model versions",
                    "auto_transitions": ["newer_version_in_production"],
                    "approval_required": False
                }
            },
            "validation_rules": {
                "performance_validation": {
                    "type": "automated",
                    "script": "validate_model_performance.py",
                    "thresholds": {
                        "accuracy": 0.85,
                        "precision": 0.80,
                        "recall": 0.80
                    }
                },
                "security_scan": {
                    "type": "automated", 
                    "script": "security_scan.py",
                    "checks": ["dependency_scan", "vulnerability_check"]
                },
                "load_testing": {
                    "type": "automated",
                    "script": "load_test.py",
                    "requirements": {
                        "max_latency_ms": 100,
                        "min_throughput_rps": 1000,
                        "max_memory_mb": 512
                    }
                }
            }
        }
        
        return workflow
    
    def validate_model_transition(self, model_name: str, 
                                 version: str,
                                 target_stage: str,
                                 governance_workflow: Dict[str, Any]) -> Dict[str, Any]:
        """Validate model transition according to governance workflow"""
        
        validation_result = {
            "model_name": model_name,
            "version": version,
            "target_stage": target_stage,
            "validation_status": "pending",
            "checks_passed": [],
            "checks_failed": [],
            "approval_status": "pending"
        }
        
        stage_config = governance_workflow["stages"].get(target_stage, {})
        requirements = stage_config.get("requirements", [])
        
        # Run validation checks
        for requirement in requirements:
            if requirement in governance_workflow["validation_rules"]:
                rule = governance_workflow["validation_rules"][requirement]
                
                if rule["type"] == "automated":
                    # Run automated validation
                    check_result = self._run_automated_validation(
                        model_name, version, requirement, rule
                    )
                    
                    if check_result["passed"]:
                        validation_result["checks_passed"].append(requirement)
                    else:
                        validation_result["checks_failed"].append({
                            "check": requirement,
                            "reason": check_result["reason"]
                        })
        
        # Determine overall validation status
        if len(validation_result["checks_failed"]) == 0:
            validation_result["validation_status"] = "passed"
        else:
            validation_result["validation_status"] = "failed"
        
        # Check approval requirements
        if stage_config.get("approval_required", False):
            validation_result["approval_required"] = True
            validation_result["approvers"] = stage_config.get("approvers", [])
        else:
            validation_result["approval_required"] = False
            validation_result["approval_status"] = "not_required"
        
        return validation_result
    
    def _run_automated_validation(self, model_name: str, 
                                 version: str,
                                 check_name: str,
                                 rule: Dict[str, Any]) -> Dict[str, Any]:
        """Run automated validation check"""
        
        try:
            if check_name == "performance_validation":
                # Load model and validate performance
                model_uri = f"models:/{model_name}/{version}"
                
                # This would load validation data and run performance checks
                # For now, we'll simulate the validation
                
                simulated_metrics = {
                    "accuracy": 0.87,
                    "precision": 0.82,
                    "recall": 0.81
                }
                
                thresholds = rule["thresholds"]
                
                for metric, value in simulated_metrics.items():
                    if metric in thresholds and value < thresholds[metric]:
                        return {
                            "passed": False,
                            "reason": f"{metric} ({value:.3f}) below threshold ({thresholds[metric]})"
                        }
                
                return {"passed": True, "metrics": simulated_metrics}
            
            elif check_name == "security_scan":
                # Run security scan
                # This would integrate with security scanning tools
                return {"passed": True, "scan_results": "No vulnerabilities found"}
            
            elif check_name == "load_testing":
                # Run load testing
                # This would integrate with load testing tools
                return {"passed": True, "performance_metrics": {"latency": 85, "throughput": 1200}}
            
            else:
                return {"passed": False, "reason": f"Unknown validation check: {check_name}"}
        
        except Exception as e:
            return {"passed": False, "reason": f"Validation error: {str(e)}"}
    
    def create_model_deployment_config(self, model_name: str, 
                                      version: str,
                                      deployment_config: Dict[str, Any]) -> Dict[str, Any]:
        """Create comprehensive model deployment configuration"""
        
        config = {
            "model": {
                "name": model_name,
                "version": version,
                "uri": f"models:/{model_name}/{version}"
            },
            "deployment": {
                "name": f"{model_name.lower()}-v{version}",
                "platform": deployment_config.get("platform", "kubernetes"),
                "replicas": deployment_config.get("replicas", 3),
                "resources": deployment_config.get("resources", {
                    "requests": {"cpu": "100m", "memory": "256Mi"},
                    "limits": {"cpu": "1", "memory": "1Gi"}
                })
            },
            "monitoring": {
                "enabled": True,
                "metrics": ["prediction_latency", "throughput", "error_rate"],
                "alerts": {
                    "latency_threshold_ms": 1000,
                    "error_rate_threshold": 0.05,
                    "drift_threshold": 0.3
                }
            },
            "autoscaling": {
                "enabled": deployment_config.get("autoscaling_enabled", True),
                "min_replicas": deployment_config.get("min_replicas", 2),
                "max_replicas": deployment_config.get("max_replicas", 10),
                "target_cpu_utilization": deployment_config.get("target_cpu", 70)
            },
            "traffic_routing": {
                "strategy": deployment_config.get("routing_strategy", "canary"),
                "canary_percentage": deployment_config.get("canary_percentage", 10),
                "success_criteria": {
                    "error_rate_threshold": 0.01,
                    "latency_p99_threshold": 500
                }
            }
        }
        
        return config
    
    def implement_a_b_testing(self, champion_model: str, 
                             challenger_model: str,
                             traffic_split: Dict[str, int]) -> Dict[str, Any]:
        """Implement A/B testing between model versions"""
        
        ab_test_config = {
            "test_id": f"ab_test_{champion_model}_vs_{challenger_model}",
            "champion": {
                "model": champion_model,
                "traffic_percentage": traffic_split.get("champion", 90)
            },
            "challenger": {
                "model": challenger_model,
                "traffic_percentage": traffic_split.get("challenger", 10)
            },
            "metrics_to_track": [
                "conversion_rate",
                "click_through_rate", 
                "prediction_accuracy",
                "prediction_latency",
                "business_kpi"
            ],
            "test_duration_days": 14,
            "significance_threshold": 0.05,
            "minimum_sample_size": 10000
        }
        
        # Generate routing configuration
        routing_config = {
            "apiVersion": "networking.istio.io/v1beta1",
            "kind": "VirtualService",
            "metadata": {
                "name": "model-ab-test"
            },
            "spec": {
                "http": [{
                    "match": [{
                        "headers": {
                            "user-group": {
                                "exact": "test"
                            }
                        }
                    }],
                    "route": [{
                        "destination": {
                            "host": f"{challenger_model}-service"
                        },
                        "weight": traffic_split.get("challenger", 10)
                    }, {
                        "destination": {
                            "host": f"{champion_model}-service"
                        },
                        "weight": traffic_split.get("champion", 90)
                    }]
                }]
            }
        }
        
        ab_test_config["routing_config"] = routing_config
        
        return ab_test_config

# Advanced MLOps automation and CI/CD integration
class MLOpsPipelineManager:
    """Advanced MLOps pipeline management and automation"""
    
    def __init__(self, mlflow_manager: MLflowExperimentManager,
                 kubeflow_manager: KubeflowPipelineManager):
        self.mlflow_manager = mlflow_manager
        self.kubeflow_manager = kubeflow_manager
        
    def create_continuous_training_pipeline(self, config: Dict[str, Any]) -> Dict[str, Any]:
        """Create continuous training pipeline configuration"""
        
        pipeline_config = {
            "name": "continuous_training_pipeline",
            "schedule": config.get("schedule", "0 2 * * *"),  # Daily at 2 AM
            "triggers": {
                "data_drift": {
                    "enabled": True,
                    "threshold": 0.3,
                    "check_interval": "6h"
                },
                "performance_degradation": {
                    "enabled": True,
                    "threshold": 0.05,  # 5% degradation
                    "check_interval": "1h"
                },
                "manual": {
                    "enabled": True
                }
            },
            "stages": [
                {
                    "name": "data_validation",
                    "component": "data_validation_component",
                    "parameters": {
                        "data_source": config["data_source"],
                        "validation_rules": config["validation_rules"]
                    }
                },
                {
                    "name": "feature_engineering",
                    "component": "feature_engineering_component",
                    "parameters": {
                        "feature_config": config["feature_config"]
                    },
                    "depends_on": ["data_validation"]
                },
                {
                    "name": "model_training",
                    "component": "model_training_component",
                    "parameters": {
                        "algorithms": config["algorithms"],
                        "hyperparameter_search": config.get("hyperparameter_search", True)
                    },
                    "depends_on": ["feature_engineering"]
                },
                {
                    "name": "model_evaluation",
                    "component": "model_evaluation_component",
                    "parameters": {
                        "quality_thresholds": config["quality_thresholds"]
                    },
                    "depends_on": ["model_training"]
                },
                {
                    "name": "model_deployment",
                    "component": "model_deployment_component",
                    "parameters": {
                        "deployment_strategy": config.get("deployment_strategy", "canary"),
                        "approval_required": config.get("approval_required", True)
                    },
                    "depends_on": ["model_evaluation"],
                    "conditions": {
                        "quality_check_passed": True,
                        "approval_received": True
                    }
                }
            ],
            "notifications": {
                "slack": {
                    "enabled": True,
                    "webhook_url": config.get("slack_webhook"),
                    "events": ["pipeline_start", "pipeline_success", "pipeline_failure", "approval_required"]
                },
                "email": {
                    "enabled": True,
                    "recipients": config.get("email_recipients", []),
                    "events": ["pipeline_failure", "approval_required"]
                }
            }
        }
        
        return pipeline_config
    
    def implement_model_monitoring(self, model_name: str, 
                                  monitoring_config: Dict[str, Any]) -> Dict[str, Any]:
        """Implement comprehensive model monitoring"""
        
        monitoring_spec = {
            "model_name": model_name,
            "monitoring_components": {
                "data_drift_detector": {
                    "enabled": True,
                    "algorithm": "ks_test",
                    "threshold": 0.05,
                    "reference_data": monitoring_config.get("reference_data_path"),
                    "check_frequency": "1h"
                },
                "performance_monitor": {
                    "enabled": True,
                    "metrics": ["accuracy", "precision", "recall", "f1_score"],
                    "baseline_performance": monitoring_config.get("baseline_performance"),
                    "degradation_threshold": 0.05,
                    "check_frequency": "1h"
                },
                "prediction_monitor": {
                    "enabled": True,
                    "sample_rate": 0.1,
                    "storage": {
                        "type": "s3",
                        "bucket": monitoring_config.get("storage_bucket"),
                        "path": f"model_predictions/{model_name}"
                    }
                },
                "latency_monitor": {
                    "enabled": True,
                    "thresholds": {
                        "p50": 100,  # ms
                        "p95": 500,  # ms
                        "p99": 1000  # ms
                    }
                },
                "error_rate_monitor": {
                    "enabled": True,
                    "threshold": 0.01,  # 1%
                    "time_window": "5m"
                }
            },
            "alerting": {
                "channels": ["slack", "email", "pagerduty"],
                "alert_rules": [
                    {
                        "name": "high_error_rate",
                        "condition": "error_rate > 0.05",
                        "severity": "critical",
                        "action": "immediate_notification"
                    },
                    {
                        "name": "data_drift_detected",
                        "condition": "drift_score > 0.3",
                        "severity": "warning",
                        "action": "schedule_retraining"
                    },
                    {
                        "name": "performance_degradation",
                        "condition": "accuracy_drop > 0.05",
                        "severity": "high",
                        "action": "rollback_consideration"
                    }
                ]
            },
            "dashboards": {
                "grafana": {
                    "enabled": True,
                    "dashboard_config": "model_monitoring_dashboard.json"
                },
                "mlflow": {
                    "enabled": True,
                    "experiment_name": f"{model_name}_monitoring"
                }
            }
        }
        
        return monitoring_spec
    
    def create_model_rollback_strategy(self, model_name: str) -> Dict[str, Any]:
        """Create automated model rollback strategy"""
        
        rollback_strategy = {
            "model_name": model_name,
            "rollback_triggers": {
                "error_rate_spike": {
                    "threshold": 0.10,  # 10% error rate
                    "time_window": "5m",
                    "action": "immediate_rollback"
                },
                "latency_spike": {
                    "threshold": 2000,  # 2 second latency
                    "time_window": "5m",
                    "action": "immediate_rollback"
                },
                "performance_degradation": {
                    "threshold": 0.15,  # 15% performance drop
                    "time_window": "30m",
                    "action": "gradual_rollback"
                },
                "manual_trigger": {
                    "action": "immediate_rollback"
                }
            },
            "rollback_procedures": {
                "immediate_rollback": {
                    "steps": [
                        "stop_traffic_to_new_model",
                        "route_traffic_to_previous_version", 
                        "notify_team",
                        "create_incident_ticket"
                    ],
                    "max_time_minutes": 5
                },
                "gradual_rollback": {
                    "steps": [
                        "reduce_traffic_to_new_model_50_percent",
                        "monitor_for_10_minutes",
                        "route_all_traffic_to_previous_version",
                        "notify_team"
                    ],
                    "max_time_minutes": 15
                }
            },
            "notification_config": {
                "immediate_channels": ["slack", "pagerduty"],
                "follow_up_channels": ["email", "jira"],
                "escalation_after_minutes": 30
            }
        }
        
        return rollback_strategy

Conclusion

Implementing enterprise-grade machine learning pipeline orchestration with MLflow and Kubeflow requires careful consideration of experiment tracking, model management, workflow automation, and production deployment strategies. The advanced patterns and implementations shown in this guide provide a comprehensive foundation for building robust, scalable, and maintainable ML operations.

Key takeaways for successful ML pipeline orchestration include:

  1. Comprehensive Tracking: Use MLflow for detailed experiment tracking and model registry management with proper governance workflows
  2. Scalable Orchestration: Leverage Kubeflow for Kubernetes-native workflow orchestration and component reusability
  3. Quality Assurance: Implement rigorous validation and testing at every stage of the ML lifecycle
  4. Monitoring and Observability: Deploy comprehensive monitoring for model performance, data drift, and system health
  5. Automated Operations: Create automated pipelines for continuous training, deployment, and rollback procedures

By combining MLflow’s experiment management capabilities with Kubeflow’s orchestration power, organizations can build world-class MLOps platforms that accelerate model development while maintaining production reliability and governance standards.