use serde::{Deserialize, Serialize};
use super::super::error::TunerError;
use super::super::features::TunerFeatures;
use super::ThroughputPrediction;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThroughputRegressor {
pub(crate) weights: Vec<f32>,
pub(crate) feature_importance: Vec<(String, f32)>,
pub(crate) sample_count: usize,
pub(crate) mape: f32,
}
impl Default for ThroughputRegressor {
fn default() -> Self {
Self::new()
}
}
impl ThroughputRegressor {
pub fn new() -> Self {
let mut weights = vec![0.0; TunerFeatures::DIM + 1];
weights[0] = 0.4;
weights[7] = 0.3;
weights[9] = 0.1;
weights[36] = 0.15;
weights[38] = 0.1;
weights[1] = -0.15;
weights[8] = -0.05;
Self {
weights,
feature_importance: Self::default_feature_importance(),
sample_count: 0,
mape: 0.15, }
}
fn default_feature_importance() -> Vec<(String, f32)> {
vec![
("batch_size".into(), 0.25),
("gpu_mem_bw".into(), 0.20),
("model_params".into(), 0.15),
("cuda_graphs".into(), 0.10),
("gpu_sm_count".into(), 0.10),
("hidden_dim".into(), 0.08),
("quant_type".into(), 0.07),
("seq_len".into(), 0.05),
]
}
pub fn train(&mut self, data: &[(TunerFeatures, f32)]) -> Result<(), TunerError> {
if data.len() < 10 {
return Err(TunerError::InsufficientData(data.len()));
}
let learning_rate = 0.01;
let epochs = 100;
for _ in 0..epochs {
let mut gradients = vec![0.0; self.weights.len()];
for (features, target) in data {
let x = features.to_vector();
let predicted = self.predict_raw(&x);
let error = predicted - target;
gradients[0] += error;
for (i, xi) in x.iter().enumerate() {
gradients[i + 1] += error * xi;
}
}
let n = data.len().max(1) as f32;
for (i, g) in gradients.iter().enumerate() {
self.weights[i] -= learning_rate * g / n;
}
}
let mut total_ape = 0.0;
for (features, target) in data {
let predicted = self.predict_raw(&features.to_vector());
total_ape += ((predicted - target) / target.max(1.0)).abs();
}
self.mape = total_ape / data.len().max(1) as f32;
self.sample_count = data.len();
Ok(())
}
pub(crate) fn predict_raw(&self, x: &[f32]) -> f32 {
let mut result = self.weights[0]; for (i, xi) in x.iter().enumerate() {
if i + 1 < self.weights.len() {
result += self.weights[i + 1] * xi;
}
}
(result * 1000.0).max(1.0)
}
pub fn predict(&self, features: &TunerFeatures) -> ThroughputPrediction {
let x = features.to_vector();
let raw_predicted_tps = self.predict_raw(&x);
let theoretical_max_tps = Self::compute_roofline_bound(features);
let predicted_tps = raw_predicted_tps.min(theoretical_max_tps);
let roofline_penalty = if raw_predicted_tps > theoretical_max_tps {
0.9 } else {
1.0
};
let confidence = (1.0 - self.mape).max(0.5) * roofline_penalty;
ThroughputPrediction {
predicted_tps,
confidence,
top_features: self.feature_importance.iter().take(5).cloned().collect(),
}
}
pub fn compute_roofline_bound(features: &TunerFeatures) -> f32 {
let model_params_b = 10.0_f32.powf(features.model_params_b * 3.0 - 1.0);
let bytes_per_param = Self::bytes_per_param_from_onehot(&features.quant_type_onehot);
let gpu_mem_bw_gbs = features.gpu_mem_bw_norm * 3000.0;
let batch_size = (features.batch_size_norm * 64.0).max(1.0);
let theoretical_max = (gpu_mem_bw_gbs * batch_size) / (model_params_b * bytes_per_param);
theoretical_max.clamp(1.0, 10000.0)
}
pub fn bytes_per_param_from_onehot(onehot: &[f32; 8]) -> f32 {
let bytes_per_param = [0.5625, 0.5625, 0.5625, 0.6875, 0.8125, 1.0, 2.0, 4.0];
let idx = onehot
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, _)| i)
.unwrap_or(0);
bytes_per_param[idx]
}
}